From 2875f1b5bc747c961899e239af68144479e007ae Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 11:30:22 -0400 Subject: [PATCH 01/20] Convert to Bundler for dependency management. --- .gemspec | 4 ++-- .gitignore | 1 + Gemfile | 4 ++++ Gemfile.lock | 23 +++++++++++++++++++++++ spec/spec_helper.rb | 4 +++- 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/.gemspec b/.gemspec index 2a7030b..98cfbd9 100644 --- a/.gemspec +++ b/.gemspec @@ -27,8 +27,8 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 1.8.2' gem.requirements = [] - gem.add_development_dependency 'rack-test', '>= 0.5.3' - gem.add_development_dependency 'rspec', '>= 1.3.0' + gem.add_development_dependency 'rack-test', '0.5.3' + gem.add_development_dependency 'rspec', '1.3.0' gem.add_development_dependency 'yard' , '>= 0.5.5' gem.add_runtime_dependency 'rack', '>= 1.0.0' gem.post_install_message = nil diff --git a/.gitignore b/.gitignore index 5cee983..6e94490 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .tmp .yardoc +.bundle pkg tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f94dcd5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +# A sample Gemfile +source "http://rubygems.org" +gemspec + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b323a79 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,23 @@ +PATH + remote: . + specs: + rack-throttle (0.3.0) + rack (>= 1.0.0) + +GEM + remote: http://rubygems.org/ + specs: + rack (1.2.2) + rack-test (0.5.3) + rack (>= 1.0) + rspec (1.3.0) + yard (0.6.8) + +PLATFORMS + ruby + +DEPENDENCIES + rack-test (= 0.5.3) + rack-throttle! + rspec (= 1.3.0) + yard (>= 0.5.5) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d73c2f..1b20ff7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,6 @@ -require "spec" +require "rubygems" +require "bundler/setup" + require "rack/test" require "rack/throttle" From 8a7e774843b69e4b3f20e4c2c85647de8254b7c9 Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 12:31:17 -0400 Subject: [PATCH 02/20] Add Timecop based time tests. --- .gemspec | 1 + Gemfile.lock | 2 ++ spec/daily_spec.rb | 10 +++++++++- spec/hourly_spec.rb | 11 ++++++++++- spec/spec_helper.rb | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.gemspec b/.gemspec index 98cfbd9..7de1ebb 100644 --- a/.gemspec +++ b/.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'rack-test', '0.5.3' gem.add_development_dependency 'rspec', '1.3.0' gem.add_development_dependency 'yard' , '>= 0.5.5' + gem.add_development_dependency 'timecop', '0.3.4' gem.add_runtime_dependency 'rack', '>= 1.0.0' gem.post_install_message = nil end diff --git a/Gemfile.lock b/Gemfile.lock index b323a79..d07aa39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,6 +11,7 @@ GEM rack-test (0.5.3) rack (>= 1.0) rspec (1.3.0) + timecop (0.3.4) yard (0.6.8) PLATFORMS @@ -20,4 +21,5 @@ DEPENDENCIES rack-test (= 0.5.3) rack-throttle! rspec (= 1.3.0) + timecop (= 0.3.4) yard (>= 0.5.5) diff --git a/spec/daily_spec.rb b/spec/daily_spec.rb index a9df1d1..e8d58eb 100644 --- a/spec/daily_spec.rb +++ b/spec/daily_spec.rb @@ -23,5 +23,13 @@ def app last_response.body.should show_throttled_response end - # TODO mess with time travelling and requests to make sure no overlap + it "should not count yesterdays requests against today" do + Timecop.freeze(Date.today - 1) do + 4.times { get "/foo" } + last_response.body.should show_throttled_response + end + + get "/foo" + last_response.body.should show_allowed_response + end end \ No newline at end of file diff --git a/spec/hourly_spec.rb b/spec/hourly_spec.rb index 93a3b1e..e3fe2dc 100644 --- a/spec/hourly_spec.rb +++ b/spec/hourly_spec.rb @@ -23,5 +23,14 @@ def app last_response.body.should show_throttled_response end - # TODO mess with time travelling and requests to make sure no overlap + it "should not count last hours requests against today" do + one_hour_ago = Time.now + Timecop.freeze(DateTime.now - 1/24.0) do + 4.times { get "/foo" } + last_response.body.should show_throttled_response + end + + get "/foo" + last_response.body.should show_allowed_response + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1b20ff7..da302b6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require "bundler/setup" require "rack/test" +require 'timecop' require "rack/throttle" def example_target_app From 44d6dda9f450fddc88f240c9358470bac27fdb7c Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 12:58:30 -0400 Subject: [PATCH 03/20] Add throttling per-minute derived from Hourly throttling. --- lib/rack/throttle.rb | 1 + lib/rack/throttle/minute.rb | 43 +++++++++++++++++++++++++++++++++++++ spec/minute_spec.rb | 36 +++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 lib/rack/throttle/minute.rb create mode 100644 spec/minute_spec.rb diff --git a/lib/rack/throttle.rb b/lib/rack/throttle.rb index fbe2005..1049a67 100644 --- a/lib/rack/throttle.rb +++ b/lib/rack/throttle.rb @@ -7,6 +7,7 @@ module Throttle autoload :TimeWindow, 'rack/throttle/time_window' autoload :Daily, 'rack/throttle/daily' autoload :Hourly, 'rack/throttle/hourly' + autoload :Minute, 'rack/throttle/minute' autoload :VERSION, 'rack/throttle/version' end end diff --git a/lib/rack/throttle/minute.rb b/lib/rack/throttle/minute.rb new file mode 100644 index 0000000..b048050 --- /dev/null +++ b/lib/rack/throttle/minute.rb @@ -0,0 +1,43 @@ +module Rack; module Throttle + ## + # This rate limiter strategy throttles the application by defining a + # maximum number of allowed HTTP requests per minute (by default, 60 + # requests per minute, which works out to an average of 1 request per + # second). + # + # Note that this strategy doesn't use a sliding time window, but rather + # tracks requests per distinct minute. This means that the throttling + # counter is reset every minute. + # + # @example Allowing up to 60 requests/minute + # use Rack::Throttle::Minute + # + # @example Allowing up to 100 requests per hour + # use Rack::Throttle::Minute, :max => 100 + # + class Minute < TimeWindow + ## + # @param [#call] app + # @param [Hash{Symbol => Object}] options + # @option options [Integer] :max (60) + def initialize(app, options = {}) + super + end + + ## + def max_per_minute + @max_per_hour ||= options[:max_per_minute] || options[:max] || 60 + end + + alias_method :max_per_window, :max_per_minute + + protected + + ## + # @param [Rack::Request] request + # @return [String] + def cache_key(request) + [super, Time.now.strftime('%Y-%m-%dT%H:%M')].join(':') + end + end +end; end diff --git a/spec/minute_spec.rb b/spec/minute_spec.rb new file mode 100644 index 0000000..de1a8ab --- /dev/null +++ b/spec/minute_spec.rb @@ -0,0 +1,36 @@ +require File.dirname(__FILE__) + '/spec_helper' + +def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Minute.new(@target_app, :max_per_minute => 3) +end + +describe Rack::Throttle::Hourly do + include Rack::Test::Methods + + it "should be allowed if not seen this hour" do + get "/foo" + last_response.body.should show_allowed_response + end + + it "should be allowed if seen fewer than the max allowed per hour" do + 2.times { get "/foo" } + last_response.body.should show_allowed_response + end + + it "should not be allowed if seen more times than the max allowed per hour" do + 4.times { get "/foo" } + last_response.body.should show_throttled_response + end + + it "should not count last minute's requests against this minute's" do + one_hour_ago = Time.now + Timecop.freeze(DateTime.now - 1/24.0/60.0) do + 4.times { get "/foo" } + last_response.body.should show_throttled_response + end + + get "/foo" + last_response.body.should show_allowed_response + end +end From 640c4116cd7dbc446646a11d6659ed89e1a07d35 Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 13:29:29 -0400 Subject: [PATCH 04/20] Properly scope app() method within describe block, fixes horrible race condition where this is over-ridden by each test you load, causing all kinds of weird failures if you run all specs as a group. --- spec/daily_spec.rb | 10 +++++----- spec/hourly_spec.rb | 10 +++++----- spec/interval_spec.rb | 10 +++++----- spec/limiter_spec.rb | 10 +++++----- spec/minute_spec.rb | 10 +++++----- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spec/daily_spec.rb b/spec/daily_spec.rb index e8d58eb..e35e342 100644 --- a/spec/daily_spec.rb +++ b/spec/daily_spec.rb @@ -1,13 +1,13 @@ require File.dirname(__FILE__) + '/spec_helper' -def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Daily.new(@target_app, :max_per_day => 3) -end - describe Rack::Throttle::Daily do include Rack::Test::Methods + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Daily.new(@target_app, :max_per_day => 3) + end + it "should be allowed if not seen this day" do get "/foo" last_response.body.should show_allowed_response diff --git a/spec/hourly_spec.rb b/spec/hourly_spec.rb index e3fe2dc..87b9126 100644 --- a/spec/hourly_spec.rb +++ b/spec/hourly_spec.rb @@ -1,13 +1,13 @@ require File.dirname(__FILE__) + '/spec_helper' -def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Hourly.new(@target_app, :max_per_hour => 3) -end - describe Rack::Throttle::Hourly do include Rack::Test::Methods + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Hourly.new(@target_app, :max_per_hour => 3) + end + it "should be allowed if not seen this hour" do get "/foo" last_response.body.should show_allowed_response diff --git a/spec/interval_spec.rb b/spec/interval_spec.rb index ecfa888..f898b1d 100644 --- a/spec/interval_spec.rb +++ b/spec/interval_spec.rb @@ -1,13 +1,13 @@ require File.dirname(__FILE__) + '/spec_helper' -def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1) -end - describe Rack::Throttle::Interval do include Rack::Test::Methods + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1) + end + it "should allow the request if the source has not been seen" do get "/foo" last_response.body.should show_allowed_response diff --git a/spec/limiter_spec.rb b/spec/limiter_spec.rb index 5f33b3b..43ba5b1 100644 --- a/spec/limiter_spec.rb +++ b/spec/limiter_spec.rb @@ -1,13 +1,13 @@ require File.dirname(__FILE__) + '/spec_helper' -def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Limiter.new(@target_app) -end - describe Rack::Throttle::Limiter do include Rack::Test::Methods + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Limiter.new(@target_app) + end + describe "basic calling" do it "should return the example app" do get "/foo" diff --git a/spec/minute_spec.rb b/spec/minute_spec.rb index de1a8ab..124519d 100644 --- a/spec/minute_spec.rb +++ b/spec/minute_spec.rb @@ -1,13 +1,13 @@ require File.dirname(__FILE__) + '/spec_helper' -def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Minute.new(@target_app, :max_per_minute => 3) -end - describe Rack::Throttle::Hourly do include Rack::Test::Methods + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Minute.new(@target_app, :max_per_minute => 3) + end + it "should be allowed if not seen this hour" do get "/foo" last_response.body.should show_allowed_response From 1a79acaab496e95ae41546724219ca8c368b8e14 Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 13:31:20 -0400 Subject: [PATCH 05/20] Update README for new Minute strategy. --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) mode change 100644 => 100755 README.md diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 0a7dfe5..721f53e --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Features * Throttles a Rack application by enforcing a minimum time interval between subsequent HTTP requests from a particular client, as well as by defining - a maximum number of allowed HTTP requests per a given time period (hourly - or daily). + a maximum number of allowed HTTP requests per a given time period (per minute, + hourly, or daily). * Compatible with any Rack application and any Rack-based framework. * Stores rate-limiting counters in any key/value store implementation that responds to `#[]`/`#[]=` (like Ruby's hashes) or to `#get`/`#set` (like @@ -60,6 +60,10 @@ Examples use Rack::Throttle::Interval, :min => 3.0 +### Allowing a maximum of 60 requests per minute + + use Rack::Throttle::Minute, :max => 60 + ### Allowing a maximum of 100 requests per hour use Rack::Throttle::Hourly, :max => 100 @@ -72,6 +76,7 @@ Examples use Rack::Throttle::Daily, :max => 1000 # requests use Rack::Throttle::Hourly, :max => 100 # requests + use Rack::Throttle::Hourly, :max => 60 # requests use Rack::Throttle::Interval, :min => 3.0 # seconds ### Storing the rate-limiting counters in a GDBM database From d1a65f1e6883b86faf289c1babbea1073c9e0b20 Mon Sep 17 00:00:00 2001 From: Winfield Peterson Date: Fri, 13 May 2011 14:13:16 -0400 Subject: [PATCH 06/20] Add an optional callback to the Limiter base strategy. This callback is fired whenever a rate limit is exceeded, to allow something like logging or per-app handling of rate limits. --- lib/rack/throttle/limiter.rb | 5 ++- spec/limiter_spec.rb | 84 +++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/rack/throttle/limiter.rb b/lib/rack/throttle/limiter.rb index bff71d7..0c15b96 100644 --- a/lib/rack/throttle/limiter.rb +++ b/lib/rack/throttle/limiter.rb @@ -31,7 +31,7 @@ def initialize(app, options = {}) # @see http://rack.rubyforge.org/doc/SPEC.html def call(env) request = Rack::Request.new(env) - allowed?(request) ? app.call(env) : rate_limit_exceeded + allowed?(request) ? app.call(env) : rate_limit_exceeded(request) end ## @@ -174,7 +174,8 @@ def request_start_time(request) # Outputs a `Rate Limit Exceeded` error. # # @return [Array(Integer, Hash, #each)] - def rate_limit_exceeded + def rate_limit_exceeded(request) + options[:rate_limit_exceeded_callback].call(request) if options[:rate_limit_exceeded_callback] headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {} http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers) end diff --git a/spec/limiter_spec.rb b/spec/limiter_spec.rb index 43ba5b1..51ce2cb 100644 --- a/spec/limiter_spec.rb +++ b/spec/limiter_spec.rb @@ -2,49 +2,65 @@ describe Rack::Throttle::Limiter do include Rack::Test::Methods - - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Limiter.new(@target_app) - end - describe "basic calling" do - it "should return the example app" do - get "/foo" - last_response.body.should show_allowed_response + describe 'with default config' do + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Limiter.new(@target_app) end - - it "should call the application if allowed" do - app.should_receive(:allowed?).and_return(true) - get "/foo" - last_response.body.should show_allowed_response + + describe "basic calling" do + it "should return the example app" do + get "/foo" + last_response.body.should show_allowed_response + end + + it "should call the application if allowed" do + app.should_receive(:allowed?).and_return(true) + get "/foo" + last_response.body.should show_allowed_response + end + + it "should give a rate limit exceeded message if not allowed" do + app.should_receive(:allowed?).and_return(false) + get "/foo" + last_response.body.should show_throttled_response + end end - - it "should give a rate limit exceeded message if not allowed" do - app.should_receive(:allowed?).and_return(false) - get "/foo" - last_response.body.should show_throttled_response + + describe "allowed?" do + it "should return true if whitelisted" do + app.should_receive(:whitelisted?).and_return(true) + get "/foo" + last_response.body.should show_allowed_response + end + + it "should return false if blacklisted" do + app.should_receive(:blacklisted?).and_return(true) + get "/foo" + last_response.body.should show_throttled_response + end + + it "should return true if not whitelisted or blacklisted" do + app.should_receive(:whitelisted?).and_return(false) + app.should_receive(:blacklisted?).and_return(false) + get "/foo" + last_response.body.should show_allowed_response + end end end - - describe "allowed?" do - it "should return true if whitelisted" do - app.should_receive(:whitelisted?).and_return(true) - get "/foo" - last_response.body.should show_allowed_response + + describe 'with rate_limit_exceeded callback' do + def app + @target_app ||= example_target_app + @app ||= Rack::Throttle::Limiter.new(@target_app, :rate_limit_exceeded_callback => lambda {|request| @app.callback(request) } ) end - - it "should return false if blacklisted" do + + it "should call rate_limit_exceeded_callback w/ request when rate limit exceeded" do app.should_receive(:blacklisted?).and_return(true) + app.should_receive(:callback).and_return(true) get "/foo" last_response.body.should show_throttled_response end - - it "should return true if not whitelisted or blacklisted" do - app.should_receive(:whitelisted?).and_return(false) - app.should_receive(:blacklisted?).and_return(false) - get "/foo" - last_response.body.should show_allowed_response - end end end \ No newline at end of file From c9d4c4bf0f6320cf45e687b6c59786996cd7c0c0 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 11:01:35 -0400 Subject: [PATCH 07/20] Gitignore Gemfile.lock --- .gitignore | 1 + Gemfile.lock | 25 ------------------------- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 Gemfile.lock diff --git a/.gitignore b/.gitignore index 6e94490..8c372b8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .bundle pkg tmp +Gemfile.lock \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index d07aa39..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,25 +0,0 @@ -PATH - remote: . - specs: - rack-throttle (0.3.0) - rack (>= 1.0.0) - -GEM - remote: http://rubygems.org/ - specs: - rack (1.2.2) - rack-test (0.5.3) - rack (>= 1.0) - rspec (1.3.0) - timecop (0.3.4) - yard (0.6.8) - -PLATFORMS - ruby - -DEPENDENCIES - rack-test (= 0.5.3) - rack-throttle! - rspec (= 1.3.0) - timecop (= 0.3.4) - yard (>= 0.5.5) From 8c8fd6603eb07487262748fb8932f452a2b6e9cb Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 11:03:13 -0400 Subject: [PATCH 08/20] Just have one gemspec --- .gemspec | 36 ------------------------------------ rack-throttle.gemspec | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 37 deletions(-) delete mode 100644 .gemspec mode change 120000 => 100644 rack-throttle.gemspec diff --git a/.gemspec b/.gemspec deleted file mode 100644 index 7de1ebb..0000000 --- a/.gemspec +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env ruby -rubygems -# -*- encoding: utf-8 -*- - -Gem::Specification.new do |gem| - gem.version = File.read('VERSION').chomp - gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') - - gem.name = 'rack-throttle' - gem.homepage = 'http://github.com/datagraph' - gem.license = 'Public Domain' if gem.respond_to?(:license=) - gem.summary = 'HTTP request rate limiter for Rack applications.' - gem.description = 'Rack middleware for rate-limiting incoming HTTP requests.' - gem.rubyforge_project = 'datagraph' - - gem.authors = ['Arto Bendiken', 'Brendon Murphy'] - gem.email = 'arto.bendiken@gmail.com' - - gem.platform = Gem::Platform::RUBY - gem.files = %w(AUTHORS README UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') - gem.bindir = %q(bin) - gem.executables = %w() - gem.default_executable = gem.executables.first - gem.require_paths = %w(lib) - gem.extensions = %w() - gem.test_files = %w() - gem.has_rdoc = false - - gem.required_ruby_version = '>= 1.8.2' - gem.requirements = [] - gem.add_development_dependency 'rack-test', '0.5.3' - gem.add_development_dependency 'rspec', '1.3.0' - gem.add_development_dependency 'yard' , '>= 0.5.5' - gem.add_development_dependency 'timecop', '0.3.4' - gem.add_runtime_dependency 'rack', '>= 1.0.0' - gem.post_install_message = nil -end diff --git a/rack-throttle.gemspec b/rack-throttle.gemspec deleted file mode 120000 index f4a047d..0000000 --- a/rack-throttle.gemspec +++ /dev/null @@ -1 +0,0 @@ -.gemspec \ No newline at end of file diff --git a/rack-throttle.gemspec b/rack-throttle.gemspec new file mode 100644 index 0000000..7de1ebb --- /dev/null +++ b/rack-throttle.gemspec @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby -rubygems +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |gem| + gem.version = File.read('VERSION').chomp + gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') + + gem.name = 'rack-throttle' + gem.homepage = 'http://github.com/datagraph' + gem.license = 'Public Domain' if gem.respond_to?(:license=) + gem.summary = 'HTTP request rate limiter for Rack applications.' + gem.description = 'Rack middleware for rate-limiting incoming HTTP requests.' + gem.rubyforge_project = 'datagraph' + + gem.authors = ['Arto Bendiken', 'Brendon Murphy'] + gem.email = 'arto.bendiken@gmail.com' + + gem.platform = Gem::Platform::RUBY + gem.files = %w(AUTHORS README UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') + gem.bindir = %q(bin) + gem.executables = %w() + gem.default_executable = gem.executables.first + gem.require_paths = %w(lib) + gem.extensions = %w() + gem.test_files = %w() + gem.has_rdoc = false + + gem.required_ruby_version = '>= 1.8.2' + gem.requirements = [] + gem.add_development_dependency 'rack-test', '0.5.3' + gem.add_development_dependency 'rspec', '1.3.0' + gem.add_development_dependency 'yard' , '>= 0.5.5' + gem.add_development_dependency 'timecop', '0.3.4' + gem.add_runtime_dependency 'rack', '>= 1.0.0' + gem.post_install_message = nil +end From 3d347833d0d3e5f4de93d673cd0fbb3885a3ea45 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 11:13:17 -0400 Subject: [PATCH 09/20] Fix spec require paths --- spec/daily_spec.rb | 2 +- spec/hourly_spec.rb | 2 +- spec/interval_spec.rb | 2 +- spec/limiter_spec.rb | 2 +- spec/minute_spec.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/daily_spec.rb b/spec/daily_spec.rb index e35e342..370ce65 100644 --- a/spec/daily_spec.rb +++ b/spec/daily_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require 'spec_helper' describe Rack::Throttle::Daily do include Rack::Test::Methods diff --git a/spec/hourly_spec.rb b/spec/hourly_spec.rb index 87b9126..781ea4a 100644 --- a/spec/hourly_spec.rb +++ b/spec/hourly_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require 'spec_helper' describe Rack::Throttle::Hourly do include Rack::Test::Methods diff --git a/spec/interval_spec.rb b/spec/interval_spec.rb index f898b1d..d1013d7 100644 --- a/spec/interval_spec.rb +++ b/spec/interval_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require 'spec_helper' describe Rack::Throttle::Interval do include Rack::Test::Methods diff --git a/spec/limiter_spec.rb b/spec/limiter_spec.rb index 51ce2cb..c7e2a12 100644 --- a/spec/limiter_spec.rb +++ b/spec/limiter_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require 'spec_helper' describe Rack::Throttle::Limiter do include Rack::Test::Methods diff --git a/spec/minute_spec.rb b/spec/minute_spec.rb index 124519d..ffaac05 100644 --- a/spec/minute_spec.rb +++ b/spec/minute_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require 'spec_helper' describe Rack::Throttle::Hourly do include Rack::Test::Methods From 68675999a16ee401487884bce9e29fb34e5d9aca Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 11:27:56 -0400 Subject: [PATCH 10/20] Use let instead of instance variables in specs --- spec/daily_spec.rb | 5 +---- spec/hourly_spec.rb | 5 +---- spec/interval_spec.rb | 5 +---- spec/limiter_spec.rb | 10 ++-------- spec/minute_spec.rb | 5 +---- spec/spec_helper.rb | 6 +++--- 6 files changed, 9 insertions(+), 27 deletions(-) diff --git a/spec/daily_spec.rb b/spec/daily_spec.rb index 370ce65..f80aca2 100644 --- a/spec/daily_spec.rb +++ b/spec/daily_spec.rb @@ -3,10 +3,7 @@ describe Rack::Throttle::Daily do include Rack::Test::Methods - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Daily.new(@target_app, :max_per_day => 3) - end + let(:app) { Rack::Throttle::Daily.new(target_app, :max_per_day => 3) } it "should be allowed if not seen this day" do get "/foo" diff --git a/spec/hourly_spec.rb b/spec/hourly_spec.rb index 781ea4a..20fa184 100644 --- a/spec/hourly_spec.rb +++ b/spec/hourly_spec.rb @@ -3,10 +3,7 @@ describe Rack::Throttle::Hourly do include Rack::Test::Methods - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Hourly.new(@target_app, :max_per_hour => 3) - end + let(:app) { Rack::Throttle::Hourly.new(target_app, :max_per_hour => 3) } it "should be allowed if not seen this hour" do get "/foo" diff --git a/spec/interval_spec.rb b/spec/interval_spec.rb index d1013d7..a115768 100644 --- a/spec/interval_spec.rb +++ b/spec/interval_spec.rb @@ -3,10 +3,7 @@ describe Rack::Throttle::Interval do include Rack::Test::Methods - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1) - end + let(:app) { Rack::Throttle::Interval.new(target_app, :min => 0.1) } it "should allow the request if the source has not been seen" do get "/foo" diff --git a/spec/limiter_spec.rb b/spec/limiter_spec.rb index c7e2a12..05af8f9 100644 --- a/spec/limiter_spec.rb +++ b/spec/limiter_spec.rb @@ -4,10 +4,7 @@ include Rack::Test::Methods describe 'with default config' do - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Limiter.new(@target_app) - end + let(:app) { Rack::Throttle::Limiter.new(target_app) } describe "basic calling" do it "should return the example app" do @@ -51,10 +48,7 @@ def app end describe 'with rate_limit_exceeded callback' do - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Limiter.new(@target_app, :rate_limit_exceeded_callback => lambda {|request| @app.callback(request) } ) - end + let(:app) { Rack::Throttle::Limiter.new(target_app, :rate_limit_exceeded_callback => lambda {|request| app.callback(request) } ) } it "should call rate_limit_exceeded_callback w/ request when rate limit exceeded" do app.should_receive(:blacklisted?).and_return(true) diff --git a/spec/minute_spec.rb b/spec/minute_spec.rb index ffaac05..e6e89a1 100644 --- a/spec/minute_spec.rb +++ b/spec/minute_spec.rb @@ -3,10 +3,7 @@ describe Rack::Throttle::Hourly do include Rack::Test::Methods - def app - @target_app ||= example_target_app - @app ||= Rack::Throttle::Minute.new(@target_app, :max_per_minute => 3) - end + let(:app) { Rack::Throttle::Minute.new(target_app, :max_per_minute => 3) } it "should be allowed if not seen this hour" do get "/foo" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index da302b6..c06b83d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,9 +5,9 @@ require 'timecop' require "rack/throttle" -def example_target_app - @target_app ||= mock("Example Rack App") - @target_app.stub!(:call).and_return([200, {}, "Example App Body"]) + +Spec::Example::ExampleGroup.instance_eval do + let(:target_app) { mock("Example Rack App", :call => [200, {}, "Example App Body"]) } end Spec::Matchers.define :show_allowed_response do From 608c722c23a8e2e9c86bf299edc29e88b56942a3 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 12:07:34 -0400 Subject: [PATCH 11/20] DRY up versioning --- .gitignore | 3 ++- VERSION | 1 - rack-throttle.gemspec | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 VERSION diff --git a/.gitignore b/.gitignore index 8c372b8..565382f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .bundle pkg tmp -Gemfile.lock \ No newline at end of file +Gemfile.lock +*.gem \ No newline at end of file diff --git a/VERSION b/VERSION deleted file mode 100644 index 0d91a54..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.0 diff --git a/rack-throttle.gemspec b/rack-throttle.gemspec index 7de1ebb..d368469 100644 --- a/rack-throttle.gemspec +++ b/rack-throttle.gemspec @@ -1,9 +1,13 @@ #!/usr/bin/env ruby -rubygems # -*- encoding: utf-8 -*- +$: << File.dirname(__FILE__) + "/lib" + +require 'rack/throttle/version' + Gem::Specification.new do |gem| - gem.version = File.read('VERSION').chomp - gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') + gem.version = Rack::Throttle::VERSION.to_s + gem.date = Date.today.to_s gem.name = 'rack-throttle' gem.homepage = 'http://github.com/datagraph' @@ -16,7 +20,7 @@ Gem::Specification.new do |gem| gem.email = 'arto.bendiken@gmail.com' gem.platform = Gem::Platform::RUBY - gem.files = %w(AUTHORS README UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') + gem.files = %w(AUTHORS README UNLICENSE) + Dir.glob('lib/**/*.rb') gem.bindir = %q(bin) gem.executables = %w() gem.default_executable = gem.executables.first From f21c4575f8f3bc4b77490e4da6a530f9b479092a Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 12:15:26 -0400 Subject: [PATCH 12/20] Declare a fork --- rack-throttle.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rack-throttle.gemspec b/rack-throttle.gemspec index d368469..c704d22 100644 --- a/rack-throttle.gemspec +++ b/rack-throttle.gemspec @@ -9,8 +9,8 @@ Gem::Specification.new do |gem| gem.version = Rack::Throttle::VERSION.to_s gem.date = Date.today.to_s - gem.name = 'rack-throttle' - gem.homepage = 'http://github.com/datagraph' + gem.name = 'viximo-rack-throttle' + gem.homepage = 'http://github.com/Viximo/rack-throttle' gem.license = 'Public Domain' if gem.respond_to?(:license=) gem.summary = 'HTTP request rate limiter for Rack applications.' gem.description = 'Rack middleware for rate-limiting incoming HTTP requests.' From 12282a3ea309fc712ff2b8bbdc4ed8c56c2aa668 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 12:29:09 -0400 Subject: [PATCH 13/20] Bump version --- lib/rack/throttle/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/throttle/version.rb b/lib/rack/throttle/version.rb index fe2bd5c..348bba9 100644 --- a/lib/rack/throttle/version.rb +++ b/lib/rack/throttle/version.rb @@ -1,7 +1,7 @@ module Rack; module Throttle module VERSION MAJOR = 0 - MINOR = 3 + MINOR = 4 TINY = 0 EXTRA = nil From 6cec6209347e99998a41a302e8ed4cbf28fddcd1 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 14:22:59 -0400 Subject: [PATCH 14/20] Fix load paths in rackups --- etc/gdbm.ru | 2 +- etc/hash.ru | 2 +- etc/memcache-client.ru | 2 +- etc/memcache.ru | 2 +- etc/memcached.ru | 2 +- etc/redis.ru | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/gdbm.ru b/etc/gdbm.ru index bc87595..70fbc20 100644 --- a/etc/gdbm.ru +++ b/etc/gdbm.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' require 'gdbm' diff --git a/etc/hash.ru b/etc/hash.ru index ba0f36d..12485d3 100644 --- a/etc/hash.ru +++ b/etc/hash.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' use Rack::Throttle::Interval, :min => 3.0, :cache => {} diff --git a/etc/memcache-client.ru b/etc/memcache-client.ru index d4385d1..aed180e 100644 --- a/etc/memcache-client.ru +++ b/etc/memcache-client.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' gem 'memcache-client' require 'memcache' diff --git a/etc/memcache.ru b/etc/memcache.ru index 2dd1502..5db637c 100644 --- a/etc/memcache.ru +++ b/etc/memcache.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' gem 'memcache' require 'memcache' diff --git a/etc/memcached.ru b/etc/memcached.ru index 5ab4e67..2f3a748 100644 --- a/etc/memcached.ru +++ b/etc/memcached.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' gem 'memcached' require 'memcached' diff --git a/etc/redis.ru b/etc/redis.ru index c3aa221..e2f2e42 100644 --- a/etc/redis.ru +++ b/etc/redis.ru @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) require 'rack/throttle' gem 'redis' require 'redis' From beb824724b2efe5eaf3b175532c980d88590fb52 Mon Sep 17 00:00:00 2001 From: TJ Singleton Date: Mon, 7 Mar 2011 16:55:02 -0500 Subject: [PATCH 15/20] return an array of strings for 1.9 http://rack.rubyforge.org/doc/files/SPEC.html --- lib/rack/throttle/limiter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/throttle/limiter.rb b/lib/rack/throttle/limiter.rb index 0c15b96..eb62c77 100644 --- a/lib/rack/throttle/limiter.rb +++ b/lib/rack/throttle/limiter.rb @@ -189,7 +189,7 @@ def rate_limit_exceeded(request) # @return [Array(Integer, Hash, #each)] def http_error(code, message = nil, headers = {}) [code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers), - http_status(code) + (message.nil? ? "\n" : " (#{message})\n")] + [http_status(code), (message.nil? ? "\n" : " (#{message})\n")]] end ## From 9e6a4a86a7178c63d0f439576580be1dea9a1f2a Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 14:31:31 -0400 Subject: [PATCH 16/20] Update rackups for 1.9.2 responses --- etc/gdbm.ru | 2 +- etc/hash.ru | 2 +- etc/memcache-client.ru | 2 +- etc/memcache.ru | 2 +- etc/memcached.ru | 2 +- etc/redis.ru | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/gdbm.ru b/etc/gdbm.ru index 70fbc20..c63ef0c 100644 --- a/etc/gdbm.ru +++ b/etc/gdbm.ru @@ -4,4 +4,4 @@ require 'gdbm' use Rack::Throttle::Interval, :min => 3.0, :cache => GDBM.new('/tmp/throttle.db') -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } diff --git a/etc/hash.ru b/etc/hash.ru index 12485d3..f019e46 100644 --- a/etc/hash.ru +++ b/etc/hash.ru @@ -3,4 +3,4 @@ require 'rack/throttle' use Rack::Throttle::Interval, :min => 3.0, :cache => {} -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } diff --git a/etc/memcache-client.ru b/etc/memcache-client.ru index aed180e..5760e97 100644 --- a/etc/memcache-client.ru +++ b/etc/memcache-client.ru @@ -5,4 +5,4 @@ require 'memcache' use Rack::Throttle::Interval, :min => 3.0, :cache => MemCache.new('localhost:11211') -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } diff --git a/etc/memcache.ru b/etc/memcache.ru index 5db637c..d3cb380 100644 --- a/etc/memcache.ru +++ b/etc/memcache.ru @@ -5,4 +5,4 @@ require 'memcache' use Rack::Throttle::Interval, :min => 3.0, :cache => Memcache.new(:server => 'localhost:11211') -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } diff --git a/etc/memcached.ru b/etc/memcached.ru index 2f3a748..e2c11f6 100644 --- a/etc/memcached.ru +++ b/etc/memcached.ru @@ -5,4 +5,4 @@ require 'memcached' use Rack::Throttle::Interval, :min => 3.0, :cache => Memcached.new -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } diff --git a/etc/redis.ru b/etc/redis.ru index e2f2e42..ecdc91a 100644 --- a/etc/redis.ru +++ b/etc/redis.ru @@ -5,4 +5,4 @@ require 'redis' use Rack::Throttle::Interval, :min => 3.0, :cache => Redis.new -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] } +run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] } From 765c326f77d96b57b68f949c635129f3154c0629 Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 14:32:42 -0400 Subject: [PATCH 17/20] Note ruby compat in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 721f53e..d74f8c7 100755 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Features [redis][] gems. * Compatible with [Heroku][]'s [memcached add-on][Heroku memcache] (currently available as a free beta service). +* Compatible with Ruby 1.8.7 & 1.9 Examples -------- From c6fa8f3b48ded4a114cbe6d408bbbeb740aabc1e Mon Sep 17 00:00:00 2001 From: Matt Griffin Date: Mon, 31 Oct 2011 14:38:43 -0400 Subject: [PATCH 18/20] Add Minute strategy to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d74f8c7..4747c84 100755 --- a/README.md +++ b/README.md @@ -105,6 +105,10 @@ Throttling Strategies * `Rack::Throttle::Interval`: Throttles the application by enforcing a minimum interval (by default, 1 second) between subsequent HTTP requests. +* `Rack::Throttle::Minute`: Throttles the application by defining a + maximum number of allowed HTTP requests per minute (by default, 60 + requests per minute, which works out to an average of 1 request per + second). * `Rack::Throttle::Hourly`: Throttles the application by defining a maximum number of allowed HTTP requests per hour (by default, 3,600 requests per 60 minutes, which works out to an average of 1 request per From e510e7038c7b23bd4f7672f14ade7936a1727d1b Mon Sep 17 00:00:00 2001 From: Weston Jossey Date: Wed, 2 Nov 2011 17:00:06 -0400 Subject: [PATCH 19/20] Improve variable names. --- lib/rack/throttle/interval.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rack/throttle/interval.rb b/lib/rack/throttle/interval.rb index 2c574a4..4e51b6e 100644 --- a/lib/rack/throttle/interval.rb +++ b/lib/rack/throttle/interval.rb @@ -27,11 +27,11 @@ def initialize(app, options = {}) # @param [Rack::Request] request # @return [Boolean] def allowed?(request) - t1 = request_start_time(request) - t0 = cache_get(key = cache_key(request)) rescue nil - allowed = !t0 || (dt = t1 - t0.to_f) >= minimum_interval + start_time = request_start_time(request) + last_call_time = cache_get(key = cache_key(request)) rescue nil + allowed = !last_call_time || (dt = start_time - last_call_time.to_f) >= minimum_interval begin - cache_set(key, t1) + cache_set(key, start_time) allowed rescue => e # If an error occurred while trying to update the timestamp stored From e8e660a242b0b55b091980d3efdc260bb19a8b60 Mon Sep 17 00:00:00 2001 From: cnrdh Date: Thu, 10 May 2012 20:18:24 +0200 Subject: [PATCH 20/20] Status 429 is now default when rate is exceeded --- README.md | 26 ++++---------------------- lib/rack/throttle/limiter.rb | 12 +++++++++--- spec/limiter_spec.rb | 14 ++++++++++++-- spec/spec_helper.rb | 8 ++++---- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 0a7dfe5..daff23b 100644 --- a/README.md +++ b/README.md @@ -130,32 +130,13 @@ override the `#client_identifier` method. HTTP Response Codes and Headers ------------------------------- -### 403 Forbidden (Rate Limit Exceeded) +In the past, various HTTP status codes has been used, but in 2012, the IETF +standardized on a new response status "429 Too Many Requests" [RFC6585] When a client exceeds their rate limit, `Rack::Throttle` by default returns -a "403 Forbidden" response with an associated "Rate Limit Exceeded" message +a "429 Too Many Requests" header with an associated "Rate Limit Exceeded" message in the response body. -An HTTP 403 response means that the server understood the request, but is -refusing to respond to it and an accompanying message will explain why. -This indicates an error on the client's part in exceeding the rate limits -outlined in the acceptable use policy for the site, service, or API. - -### 503 Service Unavailable (Rate Limit Exceeded) - -However, there exists a widespread practice of instead returning a "503 -Service Unavailable" response when a client exceeds the set rate limits. -This is technically dubious because it indicates an error on the server's -part, which is certainly not the case with rate limiting - it was the client -that committed the oops, not the server. - -An HTTP 503 response would be correct in situations where the server was -genuinely overloaded and couldn't handle more requests, but for rate -limiting an HTTP 403 response is more appropriate. Nonetheless, if you think -otherwise, `Rack::Throttle` does allow you to override the returned HTTP -status code by passing in a `:code => 503` option when constructing a -`Rack::Throttle::Limiter` instance. - Documentation ------------- @@ -211,3 +192,4 @@ information, see or the accompanying UNLICENSE file. [redis]: http://rubygems.org/gems/redis [Heroku]: http://heroku.com/ [Heroku memcache]: http://docs.heroku.com/memcache +[RFC6585]: http://tools.ietf.org/html/rfc6585 \ No newline at end of file diff --git a/lib/rack/throttle/limiter.rb b/lib/rack/throttle/limiter.rb index bff71d7..c20fc3f 100644 --- a/lib/rack/throttle/limiter.rb +++ b/lib/rack/throttle/limiter.rb @@ -10,16 +10,22 @@ module Rack; module Throttle # end # class Limiter + attr_reader :app attr_reader :options - + + CODE = 429 # http://tools.ietf.org/html/rfc6585 + MESSAGE = "Rate Limit Exceeded" + CONTENT_TYPE = "text/plain" + CHARSET = "utf-8" + ## # @param [#call] app # @param [Hash{Symbol => Object}] options # @option options [String] :cache (Hash.new) # @option options [String] :key (nil) # @option options [String] :key_prefix (nil) - # @option options [Integer] :code (403) + # @option options [Integer] :code (429) # @option options [String] :message ("Rate Limit Exceeded") def initialize(app, options = {}) @app, @options = app, options @@ -176,7 +182,7 @@ def request_start_time(request) # @return [Array(Integer, Hash, #each)] def rate_limit_exceeded headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {} - http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers) + http_error(options[:code] || CODE, options[:message] || MESSAGE, headers) end ## diff --git a/spec/limiter_spec.rb b/spec/limiter_spec.rb index 5f33b3b..02e3711 100644 --- a/spec/limiter_spec.rb +++ b/spec/limiter_spec.rb @@ -28,13 +28,13 @@ def app end describe "allowed?" do - it "should return true if whitelisted" do + it "should always return true if whitelisted" do app.should_receive(:whitelisted?).and_return(true) get "/foo" last_response.body.should show_allowed_response end - it "should return false if blacklisted" do + it "should always return false if blacklisted" do app.should_receive(:blacklisted?).and_return(true) get "/foo" last_response.body.should show_throttled_response @@ -47,4 +47,14 @@ def app last_response.body.should show_allowed_response end end + + context "limit exceeded" do + it "should return status 429 (Too Many Requests)" do + app.should_receive(:allowed?).and_return(false) + get "/foo" + last_response.status.should == 429 + end + + end + end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d73c2f..57df5b1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,13 +1,13 @@ -require "spec" +require "rspec" require "rack/test" require "rack/throttle" def example_target_app @target_app ||= mock("Example Rack App") - @target_app.stub!(:call).and_return([200, {}, "Example App Body"]) + @target_app.stub!(:call).and_return([200, {}, ["Example App Body"] ]) end -Spec::Matchers.define :show_allowed_response do +RSpec::Matchers.define :show_allowed_response do match do |body| body.include?("Example App Body") end @@ -25,7 +25,7 @@ def example_target_app end end -Spec::Matchers.define :show_throttled_response do +RSpec::Matchers.define :show_throttled_response do match do |body| body.include?("Rate Limit Exceeded") end