atomic-rate-limit-429-deauth-loop.md 2.9 KB

429 Too Many Requests как причина деавторизации

Контекст: При быстрых F5 (3-4 раза) на /places/my пользователя деавторизует — /auth/me начинает стабильно падать с 401.

Суть

Коренная причина: write-лимитер (burst=10) исчерпывается быстрее, чем refresh-механизм успевает восстановить сессию.

Схема race при старом коде

  1. Page load → refreshSession() вызывает /auth/me без Bearer → 401
  2. Page load → fetchPlaces() вызывает /places/my без Bearer → 401
  3. Оба 401 попадают в request()catchrefreshAccessToken() + ретрай с Bearer
  4. Ретрай /auth/me с Bearer → 200 (write limiter: +1)
  5. Ретрай /places/my с Bearer → 200 (write limiter: +1)
  6. Итого на 1 page load: 2 запроса к write-лимитеру. Burst=10 → исчерпан за 5 page loads

Дополнительный эффект: каскад 401 → 429

Когда burst исчерпан:

  • /auth/me → 429 (Too Many Requests)
  • request() в catch → деавторизация
  • Пользователь видит "Войти"

Исправления

1. refreshSession — сначала refresh, потом /auth/me

frontend/src/hooks/useAuth.tsx:

// было:
const res = await api.get<User>('/auth/me')
// стало:
const newToken = await refreshAccessToken()
setAccessToken(newToken)
const res = await api.get<User>('/auth/me')

Теперь /auth/me приходит с Bearer с первого раза — ни 401, ни ретрая. Сокращает write-лимитер с 2 до 1 хита на page load.

2. Экспорт refreshAccessToken из api.ts

Нужен для прямого вызова из refreshSession().

3. 429 retry в request()

frontend/src/lib/api.ts:

if (res.status === 429 && !isRetry) {
    await new Promise(r => setTimeout(r, 1000))
    return request<T>(path, options, true)
}

4. Увеличение write-лимитера 10 → 60 req/min

  • backend/cmd/api/main.go (in-memory fallback): 10/60.0, 1060/60.0, 60
  • backend/internal/middleware/ratelimit_redis.go (Redis): Limit: 10Limit: 60

Файлы:

  • frontend/src/hooks/useAuth.tsx:38-41
  • frontend/src/lib/api.ts:19 (экспорт)
  • frontend/src/lib/api.ts:72-75 (429 retry)
  • backend/cmd/api/main.go:149
  • backend/internal/middleware/ratelimit_redis.go:146

Связанные заметки

  • [[atomic-refresh-token-race-condition]]
  • [[atomic-cookie-secure-via-protocol]]
  • [[atomic-fetch-retry-infinite-loop]]
  • [[atomic-redis-rate-limiter-failopen-failclosed]]

#auth #rate-limiting #frontend #backend #bugfix