Manchmal muss auch eine API Grenzen setzen. Das kann beispielsweise sein, wenn sie sich einem (D)DoS-Angriff ausgesetzt sieht, oder auch wenn ein legitimer Client sich in einer Retry-Endlosschleife verfangen hat. Wenn die API in Ruby on Rails implementiert ist, war für solche rate limits lange das Gem rack-attack das Mittel der Wahl. Seit Rails 7.2 unterstützt das Framework rate limits auch von Haus aus.

Dazu fügt man einfach rate_limit mit den gewünschten Parametern in den Controller ein. Das kann der zentrale ApplicationController sein, für ein rate limit auf die gesamte API, oder ein spezifischer Controller. Zu den wichtigsten Parametern gehören

  • to: Gewünschte Anzahl an requests, die maximal erlaubt sein soll
  • within: Das Zeitfenster
  • only: Auf welche actions das limit angewendet werden soll
  • except: Auf welche actions das limit nicht angewendet werden soll

Für ein rate limit von maximal 100 requests pro Minute, angewandt nur auf index, wäre das entsprechend

rate_limit to: 100,
           within: 1.minute,
           only: :index

Ab dem einhunderersten request reagiert die API mit dem HTTP status code 429 Too Many Requests.

Wichtig ist, dass das rate limit den Rails-eigenen cache verwendet. Der muss also im jeweiligen environment zur Verfügung stehen. Sonst kann man Überraschungen erleben, wenn beispielsweise in config/environments/test.rb die Einstellung cache_store auf dem Standard :null_store steht. Dann nämlich gehen in der test suite beliebig viele requests durch trotz rate limit, weil die App sich schlicht nicht “notieren” kann, wie viele denn schon da waren. Besser ist config.cache_store = :memory_store. Für andere environments muss man entsprechend einen geeigneten cache store bereitstellen.

Oder jedenfalls einen store, in dem die Daten abgelegt werden kann. Man kann nämlich konfigurieren, wo das sein soll:

RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
rate_limit to: 100,
           within: 1.minute,
           store: RATE_LIMIT_STORE

Außerdem kann man mit Hilfe eines Lamdas festlegen, woran die App festmachen soll, wer genau da anklopft. Standardmäßig gilt das limit pro request.remote_ip. Wenn man beispielsweise ein Limit pro Benutzer möchte, könnte das so aussehen:

rate_limit to: 100,
           within: 1.minute,
           by: -> { current_user.id }

Auch die Reaktion, wenn das limit überschritten wurde, kann man mit einem Lamda anpassen:

rate_limit to: 100,
           within: 1.minute,
           with: -> {
                      redirect_to login_path
                      alert: "Too many attempts"
                    }

Mit Hilfe des Parameters name kann man sogar mehrere rate limits im selben Controller unterscheidbar machen.

Die Entwicklung ist damit noch nicht zu Ende. Ein erst kürzlich gemergter PR ermöglicht es beispielsweise, für die Parameter to und within nicht nur feste Werte anzugeben. Zukünftig sollen dann an dieser Stelle auch Instanzmethoden oder callables (lambdas oder procs) möglich sein:

rate_limit to: :max_requests,
           within: :time_window,
           by: -> { current_user.id }

private

def max_requests
  current_user.premium? ? 1000 : 100
end

def time_window
  current_user.premium? ? 1.hour : 1.minute
end

Diese Möglichkeit ist aber wie gesagt noch ein Stück weit Zukunftsmusik und wird erst in einer kommenden Rails-Version allgemein verfügbar sein.