Files
new-api/controller/responses_websocket.go
T
CaIon 977450a3e8 fix(rate-limit): explain model rate-limit rejections without Redis
Without Redis, the in-memory model request limiter rejected requests
with a bare 429: no error body and no log line. Clients could not tell
it from an upstream 429, and operators could not see which user hit the
limit.

It now returns the same error message and writes the same log line as
the Redis limiter, for both the total and the success limit.

Fixes #7721
2026-10-11 13:56:49 +08:00

129 lines
4.5 KiB
Go

package controller
import (
"bytes"
"context"
"errors"
"net"
"net/http"
"sync"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/relay"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/gin-gonic/gin"
)
type responsesWSRequestContextKey struct{}
type responsesWSRequestState struct {
requestID string
handle func(*gin.Context) *types.NewAPIError
apiError *types.NewAPIError
}
// Each response.create runs the ordinary request middleware to completion,
// without routing another HTTP request or retaining a pooled Gin context.
var responsesWSRequestEngine = sync.OnceValue(func() *gin.Engine {
engine := gin.New()
engine.ForwardedByClientIP = false
_ = engine.SetTrustedProxies(nil)
engine.POST("/v1/responses", func(c *gin.Context) {
state := c.Request.Context().Value(responsesWSRequestContextKey{}).(*responsesWSRequestState)
c.Set(common.RequestIdKey, state.requestID)
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
c.Next()
}, middleware.BodyStorageCleanup(), middleware.TokenAuth(), middleware.ModelRequestRateLimit(), func(c *gin.Context) {
state := c.Request.Context().Value(responsesWSRequestContextKey{}).(*responsesWSRequestState)
state.apiError = state.handle(c)
if state.apiError != nil {
status := state.apiError.StatusCode
if status < http.StatusBadRequest {
status = http.StatusInternalServerError
}
c.Status(status)
}
})
return engine
})
type responsesWSResponseWriter struct {
header http.Header
status int
body bytes.Buffer
}
func (w *responsesWSResponseWriter) Header() http.Header {
return w.header
}
func (w *responsesWSResponseWriter) WriteHeader(status int) {
if w.status == 0 {
w.status = status
}
}
func (w *responsesWSResponseWriter) Write(data []byte) (int, error) {
w.WriteHeader(http.StatusOK)
return w.body.Write(data)
}
func newResponsesWSRequestRunner(c *gin.Context) relay.ResponsesWSRequestRunner {
// Capture credentials before channel selection or header overrides. Resolve
// the peer once with the public router's trusted-proxy configuration.
headers := c.Request.Header.Clone()
for _, name := range []string{"Connection", "Upgrade", "Sec-WebSocket-Key", "Sec-WebSocket-Version", "Sec-WebSocket-Extensions", "Sec-WebSocket-Protocol", "Content-Length", "Content-Encoding"} {
headers.Del(name)
}
remoteAddr := net.JoinHostPort(c.ClientIP(), "0")
return func(request *http.Request, requestID string, handle func(*gin.Context) *types.NewAPIError) *types.NewAPIError {
state := &responsesWSRequestState{requestID: requestID, handle: handle}
ctx := context.WithValue(request.Context(), responsesWSRequestContextKey{}, state)
ctx = context.WithValue(ctx, common.RequestIdKey, requestID)
request = request.Clone(ctx)
request.Method = http.MethodPost
request.URL.Path = "/v1/responses"
request.URL.RawPath = ""
request.Header = headers.Clone()
request.Header.Set("Content-Type", "application/json")
request.RemoteAddr = remoteAddr
response := &responsesWSResponseWriter{header: make(http.Header)}
responsesWSRequestEngine().ServeHTTP(response, request)
if state.apiError != nil {
return state.apiError
}
if response.status < http.StatusBadRequest {
return nil
}
var body struct {
Error *types.OpenAIError `json:"error"`
}
if common.Unmarshal(response.body.Bytes(), &body) == nil && body.Error != nil {
return types.WithOpenAIError(*body.Error, response.status, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
// A rejection without an OpenAI error body keeps its HTTP status text.
return types.NewErrorWithStatusCode(errors.New(http.StatusText(response.status)), types.ErrorCodeInvalidRequest, response.status, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
}
func ResponsesWebSocket(c *gin.Context) {
requestID := c.GetString(common.RequestIdKey)
runner := newResponsesWSRequestRunner(c)
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer ws.Close()
if apiError := relay.ResponsesWebSocketHelper(c, ws, runner); apiError != nil {
logger.LogError(c, common.LogText("responses websocket relay error: %s", common.LocalLogPreview(apiError.Error())))
apiError.SetMessage(common.MessageWithRequestId(apiError.Error(), requestID))
helper.WssError(c, ws, apiError.ToOpenAIError())
}
}