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



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/tina4/middleware.rb', line 189

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



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/tina4/middleware.rb', line 228

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)



251
252
253
# File 'lib/tina4/middleware.rb', line 251

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