fix(auth): keep login state on rate-limited or failing token refresh

When the dashboard token refresh endpoint returned 429 (shared
critical rate limit) the frontend classified it as out_of_sync,
cleared local auth state, and redirected to /sign-in. The rate limit
itself is working as intended; the bug is that a temporary rejection
was treated as a terminal auth failure.

- Treat 429 refresh responses as transient errors on the frontend,
  keeping the session retryable instead of clearing it. Only explicit
  401 or confirmed session mismatch/race exhaustion clears auth state.
- Return Retry-After on all rate-limited responses (remaining TTL on
  Redis, window duration on the in-memory limiter) so clients can
  back off.
- Log the underlying error with request context when auth session
  errors map to 500 AUTH_INTERNAL_ERROR, and replace fmt.Println with
  request-scoped logging in the Redis rate limiter error paths.

Fixes #6361
This commit is contained in:
CaIon
2026-07-21 12:39:29 +08:00
parent e0d5156115
commit 1721144221
5 changed files with 57 additions and 14 deletions
+21 -12
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/logger"
"github.com/gin-gonic/gin"
)
@@ -106,33 +107,43 @@ func redisFixedWindowTake(ctx context.Context, key string, maxRequestNum int, du
}
func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
allowed, _, _, err := redisFixedWindowTake(
allowed, _, ttlSeconds, err := redisFixedWindowTake(
c.Request.Context(),
redisIPRateLimitKey(mark, c.ClientIP()),
maxRequestNum,
duration,
)
if err != nil {
fmt.Println(err.Error())
logger.LogError(c.Request.Context(), fmt.Sprintf("rate limit check failed (mark=%s): %v", mark, err))
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
if !allowed {
c.Status(http.StatusTooManyRequests)
c.Abort()
writeRateLimited(c, ttlSeconds)
}
}
func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
key := mark + c.ClientIP()
if !inMemoryRateLimiter.Request(key, maxRequestNum, duration) {
c.Status(http.StatusTooManyRequests)
c.Abort()
writeRateLimited(c, duration)
return
}
}
// writeRateLimited rejects the request with 429 and a Retry-After hint so
// clients can back off instead of treating the rejection as a fatal error.
// The in-memory limiter cannot report the remaining window, so callers
// without a TTL pass the full window duration as a conservative upper bound.
func writeRateLimited(c *gin.Context, retryAfterSeconds int64) {
if retryAfterSeconds > 0 {
c.Header("Retry-After", strconv.FormatInt(retryAfterSeconds, 10))
}
c.Status(http.StatusTooManyRequests)
c.Abort()
}
func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) {
if common.RedisEnabled {
return func(c *gin.Context) {
@@ -201,8 +212,7 @@ func userRateLimitFactory(maxRequestNum int, duration int64, mark string) func(c
}
key := fmt.Sprintf("%s:user:%d", mark, userID)
if !inMemoryRateLimiter.Request(key, maxRequestNum, duration) {
c.Status(http.StatusTooManyRequests)
c.Abort()
writeRateLimited(c, duration)
return
}
}
@@ -211,16 +221,15 @@ func userRateLimitFactory(maxRequestNum int, duration int64, mark string) func(c
// userRedisRateLimiter is like redisRateLimiter but accepts a pre-built key
// (to support user-ID-based keys).
func userRedisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, key string) {
allowed, _, _, err := redisFixedWindowTake(c.Request.Context(), key, maxRequestNum, duration)
allowed, _, ttlSeconds, err := redisFixedWindowTake(c.Request.Context(), key, maxRequestNum, duration)
if err != nil {
fmt.Println(err.Error())
logger.LogError(c.Request.Context(), fmt.Sprintf("rate limit check failed (key=%s): %v", key, err))
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
if !allowed {
c.Status(http.StatusTooManyRequests)
c.Abort()
writeRateLimited(c, ttlSeconds)
}
}
+3 -1
View File
@@ -61,7 +61,9 @@ func TestRedisIPRateLimiterThresholdTTLAndNamespace(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, performRateLimitRequest(router, "/limited", remoteAddr).Code)
assert.Equal(t, http.StatusNoContent, performRateLimitRequest(router, "/limited", remoteAddr).Code)
assert.Equal(t, http.StatusTooManyRequests, performRateLimitRequest(router, "/limited", remoteAddr).Code)
limitedResponse := performRateLimitRequest(router, "/limited", remoteAddr)
assert.Equal(t, http.StatusTooManyRequests, limitedResponse.Code)
assert.Equal(t, "37", limitedResponse.Header().Get("Retry-After"))
key := redisIPRateLimitKey("TEST", "192.0.2.10")
count, err := redisServer.Get(key)