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
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/tina4/middleware.rb', line 447 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
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 |
# File 'lib/tina4/middleware.rb', line 486 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)
509 510 511 |
# File 'lib/tina4/middleware.rb', line 509 def reset! @mutex.synchronize { @store.clear } end |