Class: Tina4::RateLimiterMiddleware

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/middleware.rb

Overview

RateLimiterMiddleware – tracks requests per IP, returns 429 when exceeded. Config via env: TINA4_RATE_LIMIT (default 100), TINA4_RATE_WINDOW (default 60s).

Class Method Summary collapse

Class Method Details

.before_rate_limit(request, response) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/tina4/middleware.rb', line 312

def before_rate_limit(request, response)
  limit  = (ENV["TINA4_RATE_LIMIT"]  || 100).to_i
  window = (ENV["TINA4_RATE_WINDOW"] || 60).to_i
  ip = request.ip || "unknown"
  now = Time.now

  cleanup_if_needed(now, window)

  @mutex.synchronize do
    @store[ip] ||= []
    entries = @store[ip]

    # Sliding window -- drop expired timestamps
    cutoff = now - window
    entries.reject! { |t| t < cutoff }

    if entries.length >= limit
      oldest = entries.first
      retry_after = [(oldest + window - now).ceil, 1].max

      response.headers["X-RateLimit-Limit"]     = limit.to_s
      response.headers["X-RateLimit-Remaining"]  = "0"
      response.headers["X-RateLimit-Reset"]      = (oldest + window).to_i.to_s
      response.headers["Retry-After"]            = retry_after.to_s
      response.json({ error: "Too Many Requests", retry_after: retry_after }, 429)

      return [request, response]
    end

    entries << now

    response.headers["X-RateLimit-Limit"]     = limit.to_s
    response.headers["X-RateLimit-Remaining"]  = (limit - entries.length).to_s
    response.headers["X-RateLimit-Reset"]      = (now + window).to_i.to_s
  end

  [request, response]
end

.check(ip) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/tina4/middleware.rb', line 351

def check(ip)
  limit  = (ENV["TINA4_RATE_LIMIT"]  || 100).to_i
  window = (ENV["TINA4_RATE_WINDOW"] || 60).to_i
  now = Time.now

  @mutex.synchronize do
    @store[ip] ||= []
    entries = @store[ip]
    entries.reject! { |t| t < now - window }

    remaining = [limit - entries.length, 0].max
    reset_at  = entries.empty? ? window : (entries.first + window - now).ceil

    if entries.length >= limit
      return [false, { limit: limit, remaining: 0, reset: reset_at, window: window }]
    end

    entries << now
    [true, { limit: limit, remaining: remaining - 1, reset: window, window: window }]
  end
end

.reset!Object

Allow resetting state (useful in tests)



374
375
376
# File 'lib/tina4/middleware.rb', line 374

def reset!
  @mutex.synchronize { @store.clear }
end