Class: Tina4::RateLimiterMiddleware
- Inherits:
-
Object
- Object
- Tina4::RateLimiterMiddleware
- 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
- .before_rate_limit(request, response) ⇒ Object
- .check(ip) ⇒ Object
-
.reset! ⇒ Object
Allow resetting state (useful in tests).
Class Method Details
.before_rate_limit(request, response) ⇒ Object
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 |
# File 'lib/tina4/middleware.rb', line 480 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
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 |
# File 'lib/tina4/middleware.rb', line 519 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)
542 543 544 |
# File 'lib/tina4/middleware.rb', line 542 def reset! @mutex.synchronize { @store.clear } end |