rack-berater 0.0.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1fad5e57124897f72b0fac11bf460c6b3dc279d057f56f22127ec32274d782d
4
- data.tar.gz: aa7e1c738a450ff6fb72aa9b92e1211ed51c544f801a2ffef475a2a89805db5c
3
+ metadata.gz: 5840eaff6319d5e24c78c2f50960138ca27fcce25bb3d45354e7eca195b50f0d
4
+ data.tar.gz: f9cdde9f38c97d82e03d6a1e9cea2e1939eaca13f4202a2a674a6fda4e6f530e
5
5
  SHA512:
6
- metadata.gz: 0ec05fa53c35aea22a9c29e93b5d509c8a161364542641ada1af1c421320d671cd634dc8ab74abc597f38a056fb3d8638b69786cf82e26357b0ace12bf483ee9
7
- data.tar.gz: c9f1c61d05ac9f297b88fad401fdf635cd553a15d2040a2fc7e45282fe2776e00967b8e7181cec5a621c5cb69771676cf7d70a34ee63e6be2b74dd7784b7b7d2
6
+ metadata.gz: b0f561957104a0d049d161b720615a97474439c41f16507b3bd34a1f68269fccbea314f54736393a5e030abdf755de8ae30bc5a92724502371728975b6cf7460
7
+ data.tar.gz: f91a4cb03f24b7c18554c8145375a78f1b61cc66a2d99a0841ddca4b547dc97822bd99dfe9267ae1feeb01d3e2c8eba72d70ea075d367671f3e2077e404a8e36
@@ -0,0 +1,90 @@
1
+ require 'rack'
2
+
3
+ module Rack
4
+ class Berater
5
+ class Prioritizer
6
+ ENV_KEY = 'berater_priority'
7
+ HEADER = 'X-Berater-Priority'
8
+
9
+ def initialize(app, options = {})
10
+ @app = app
11
+ @header = options[:header] || HEADER
12
+ end
13
+
14
+ def call(env)
15
+ priority = env[@header] || env["HTTP_#{@header.upcase.tr('-', '_')}"]
16
+
17
+ if priority
18
+ set_priority(priority)
19
+ return @app.call(env)
20
+ end
21
+
22
+ cache_key = cache_key_for(env)
23
+ cached_priority = cache_get(cache_key)
24
+
25
+ if cached_priority
26
+ set_priority(cached_priority)
27
+ end
28
+
29
+ @app.call(env).tap do |status, headers, body|
30
+ app_priority = headers.delete(@header) if headers
31
+
32
+ if app_priority && app_priority != cached_priority
33
+ # update cache for next time
34
+ cache_set(cache_key, app_priority)
35
+ end
36
+ end
37
+ ensure
38
+ Thread.current[ENV_KEY] = nil
39
+ end
40
+
41
+ def self.current_priority
42
+ Thread.current[ENV_KEY]
43
+ end
44
+
45
+ protected
46
+
47
+ def set_priority(priority)
48
+ Thread.current[ENV_KEY] = priority
49
+ end
50
+
51
+ @@cache_prefix = 'Berater:Rack:Prioritizer'
52
+ def cache_key_for(env)
53
+ req = Rack::Request.new(env)
54
+
55
+ req_method = env[Rack::REQUEST_METHOD].downcase
56
+ path = ''
57
+
58
+ if defined?(Rails) && Rails.application
59
+ res = Rails.application.routes.recognize_path(env[Rack::PATH_INFO])
60
+ path = res.values_at(:controller, :action).compact.join('#')
61
+ end
62
+
63
+ if path.empty?
64
+ path = env['PATH_INFO'].gsub(%r{/[0-9]+(/|$)}, '/x\1')
65
+ end
66
+
67
+ [
68
+ @@cache_prefix,
69
+ req_method,
70
+ path,
71
+ ].join(':')
72
+ end
73
+
74
+ @@cache = {}
75
+
76
+ def cache_get(key)
77
+ synchronize { @@cache[key] }
78
+ end
79
+
80
+ def cache_set(key, priority)
81
+ synchronize { @@cache[key] = priority }
82
+ end
83
+
84
+ @@lock = Thread::Mutex.new
85
+ def synchronize(&block)
86
+ @@lock.synchronize(&block)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,9 +1,13 @@
1
1
  require 'rails/railtie'
2
2
 
3
3
  module Rack
4
- module Berater
4
+ class Berater
5
5
  class Railtie < Rails::Railtie
6
- initializer "rack.berater.initializer" do |app|
6
+ initializer 'rack.berater' do |app|
7
+ if ::Berater.middleware.include?(::Berater::Middleware::LoadShedder)
8
+ app.middleware.use Rack::Berater::Prioritizer
9
+ end
10
+
7
11
  app.middleware.use Rack::Berater
8
12
  end
9
13
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Berater
3
- VERSION = "0.0.1"
2
+ class Berater
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/rack/berater.rb CHANGED
@@ -1,9 +1,67 @@
1
- require "rack/berater/version"
1
+ require 'berater'
2
+ require 'rack'
3
+ require 'rack/berater/version'
4
+ require 'set'
2
5
 
3
6
  module Rack
4
- module Berater
5
- autoload :Handler, "rack/berater/handler"
6
- # autoload :Limiter, "rack/berater/limiter"
7
- autoload :Railtie, "rack/berater/railtie"
7
+ class Berater
8
+ autoload :Prioritizer, 'rack/berater/prioritizer'
9
+ autoload :Railtie, 'rack/berater/railtie'
10
+
11
+ ERRORS = Set[ ::Berater::Overloaded ]
12
+
13
+ def initialize(app, options = {})
14
+ @app = app
15
+ @enabled = options[:enabled?]
16
+ @limiter = options[:limiter]
17
+ @options = {
18
+ headers: {},
19
+ status_code: options.fetch(:status_code, 429),
20
+ }
21
+
22
+ # configure body
23
+ @options[:body] = case options[:body]
24
+ when true, nil
25
+ Rack::Utils::HTTP_STATUS_CODES[@options[:status_code]]
26
+ when false
27
+ nil
28
+ when String
29
+ options[:body]
30
+ else
31
+ raise ArgumentError, 'invalid :body option: #{options[:body]}'
32
+ end
33
+
34
+ # configure headers
35
+ if @options[:body]
36
+ @options[:headers][Rack::CONTENT_TYPE] = 'text/plain'
37
+ end
38
+ @options[:headers].update(options.fetch(:headers, {}))
39
+ end
40
+
41
+ def call(env)
42
+ if enabled?(env)
43
+ limit(env) { @app.call(env) }
44
+ else
45
+ @app.call(env)
46
+ end
47
+ rescue *ERRORS => e
48
+ [
49
+ @options[:status_code],
50
+ @options[:headers],
51
+ [ @options[:body] ].compact,
52
+ ]
53
+ end
54
+
55
+ private
56
+
57
+ def enabled?(env)
58
+ return false unless @limiter
59
+ @enabled.nil? ? true : @enabled.call(env)
60
+ end
61
+
62
+ def limit(env, &block)
63
+ limiter = @limiter.respond_to?(:call) ? @limiter.call(env) : @limiter
64
+ limiter.limit(&block)
65
+ end
8
66
  end
9
67
  end
data/lib/rack-berater.rb CHANGED
@@ -1,3 +1 @@
1
- require "berater"
2
- require "rack"
3
- require "rack/berater"
1
+ require 'rack/berater'
@@ -0,0 +1,118 @@
1
+ describe Rack::Berater do
2
+ before do
3
+ app.use described_class, limiter: limiter, enabled?: enabled?
4
+ end
5
+ let(:limiter) { nil }
6
+ let(:enabled?) { nil }
7
+
8
+ let(:app) do
9
+ Rack::Builder.new do
10
+ use Rack::Lint
11
+ run (lambda do |env|
12
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
13
+ end)
14
+ end
15
+ end
16
+ let(:response) { get '/' }
17
+
18
+ shared_examples 'works nominally' do
19
+ it 'has the correct status code' do
20
+ expect(response.status).to eq 200
21
+ end
22
+
23
+ it 'has the correct headers' do
24
+ expect(response.headers).to eq({
25
+ 'Content-Type' => 'text/plain',
26
+ 'Content-Length' => '2',
27
+ })
28
+ end
29
+
30
+ it 'has the correct body' do
31
+ expect(response.body).to eq 'OK'
32
+ end
33
+ end
34
+
35
+ context 'without a limiter' do
36
+ before { Berater.test_mode = :fail }
37
+
38
+ include_examples 'works nominally'
39
+ end
40
+
41
+ describe 'limiter option' do
42
+ context 'when limiter is a limiter' do
43
+ let(:limiter) { ::Berater::Unlimiter.new }
44
+
45
+ include_examples 'works nominally'
46
+
47
+ it 'calls the limiter' do
48
+ expect(limiter).to receive(:limit).and_call_original
49
+ response
50
+ end
51
+
52
+ context 'when operating beyond limits' do
53
+ before { Berater.test_mode = :fail }
54
+
55
+ it 'returns an error' do
56
+ expect(response.status).to eq 429
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'when limiter is a proc' do
62
+ let(:limiter_instance) { ::Berater::Unlimiter.new }
63
+ let(:limiter) { Proc.new { limiter_instance } }
64
+
65
+ include_examples 'works nominally'
66
+
67
+ it 'calls the proc with env' do
68
+ expect(limiter).to receive(:call).with(Hash).and_call_original
69
+ response
70
+ end
71
+
72
+ context 'when operating beyond limits' do
73
+ before { Berater.test_mode = :fail }
74
+
75
+ it 'returns an error' do
76
+ expect(response.status).to eq 429
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'enabled? option' do
83
+ after { expect(response.status).to eq 200 }
84
+
85
+ let(:enabled?) { double }
86
+
87
+ context 'when there is a limiter' do
88
+ let(:limiter) { ::Berater::Unlimiter.new }
89
+
90
+ it 'should be called with the env hash' do
91
+ expect(enabled?).to receive(:call) do |env|
92
+ expect(env).to be_a Hash
93
+ expect(Rack::Request.new(env).path).to eq '/'
94
+ end
95
+ end
96
+
97
+ context 'when enabled' do
98
+ it 'should call the limiter' do
99
+ expect(enabled?).to receive(:call).and_return(true)
100
+ expect(limiter).to receive(:limit).and_call_original
101
+ end
102
+ end
103
+
104
+ context 'when disabled' do
105
+ it 'should not call the limiter' do
106
+ expect(enabled?).to receive(:call).and_return(false)
107
+ expect(limiter).not_to receive(:limit)
108
+ end
109
+ end
110
+ end
111
+
112
+ context 'when there is no limiter' do
113
+ it 'should not call enabled?' do
114
+ expect(enabled?).not_to receive(:call)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,295 @@
1
+ describe Rack::Berater::Prioritizer do
2
+ after do
3
+ cache.clear
4
+ Thread.current[described_class::ENV_KEY] = nil
5
+ end
6
+
7
+ let(:cache) { described_class.class_variable_get(:@@cache) }
8
+
9
+ describe '#call' do
10
+ subject { described_class.new(app) }
11
+
12
+ let(:app) { ->(*) { [200, {'Content-Type' => 'text/plain'}, ['OK']] } }
13
+ let(:cache_key) { subject.send(:cache_key_for, env) }
14
+ let(:env) { Rack::MockRequest.env_for('/') }
15
+
16
+ specify 'sanity check' do
17
+ expect(app).to receive(:call).and_call_original
18
+
19
+ Rack::Lint.new(subject).call(env)
20
+ end
21
+
22
+ it 'checks the cache' do
23
+ is_expected.to receive(:cache_get)
24
+
25
+ subject.call(env)
26
+ end
27
+
28
+ context 'with a cached priority' do
29
+ before do
30
+ allow(subject).to receive(:cache_get).with(cache_key).and_return(priority)
31
+ end
32
+
33
+ let(:priority) { '3' }
34
+
35
+ after { subject.call(env) }
36
+
37
+ it 'sets the priority accordingly' do
38
+ is_expected.to receive(:set_priority).with(priority)
39
+ end
40
+
41
+ it 'updates the global priority during the request' do
42
+ expect(app).to receive(:call) do
43
+ expect(described_class.current_priority).to eq priority
44
+ end
45
+ end
46
+
47
+ it 'resets the priority after the request completes' do
48
+ subject.call(env)
49
+ expect(described_class.current_priority).to be nil
50
+ end
51
+ end
52
+
53
+ context 'with an incoming priority header' do
54
+ let(:env) do
55
+ Rack::MockRequest.env_for(
56
+ '/',
57
+ described_class::HEADER => priority,
58
+ )
59
+ end
60
+ let(:priority) { '2' }
61
+
62
+ after { subject.call(env) }
63
+
64
+ it 'uses the header' do
65
+ is_expected.to receive(:set_priority).with(priority)
66
+ end
67
+
68
+ it 'ignores any cached value' do
69
+ allow(subject).to receive(:cache_get).with(cache_key).and_return('123')
70
+ is_expected.to receive(:set_priority).with(priority)
71
+ end
72
+
73
+ it 'resets the priority after the request completes' do
74
+ subject.call(env)
75
+ expect(described_class.current_priority).to be nil
76
+ end
77
+ end
78
+
79
+ context 'when the app returns a priority header' do
80
+ let(:app) do
81
+ ->(*) { [200, { described_class::HEADER => priority }, ['OK']] }
82
+ end
83
+
84
+ let(:priority) { '5' }
85
+
86
+ after { subject.call(env) }
87
+
88
+ it 'caches the priority' do
89
+ is_expected.to receive(:cache_set).with(cache_key, priority)
90
+ end
91
+
92
+ it 'removes the header' do
93
+ _, headers, _ = subject.call(env)
94
+ expect(headers).not_to include described_class::HEADER
95
+ end
96
+
97
+ it 'updates the cache when a different priority is returned' do
98
+ expect(subject).to receive(:cache_get).and_return('123')
99
+ is_expected.to receive(:cache_set).with(cache_key, priority)
100
+ end
101
+
102
+ it 'does not update the cache when the same priority is returned' do
103
+ expect(subject).to receive(:cache_get).and_return(priority)
104
+ is_expected.not_to receive(:cache_set)
105
+ end
106
+ end
107
+
108
+ it 'does not update the cache when no priority is returned' do
109
+ is_expected.not_to receive(:cache_set)
110
+ subject.call(env)
111
+ end
112
+ end
113
+
114
+ describe '#cache_key_for' do
115
+ subject{ described_class.new(nil).send(:cache_key_for, env) }
116
+
117
+ context 'with a basic env' do
118
+ let(:env) { Rack::MockRequest.env_for('/') }
119
+
120
+ it 'has a Berater prefix' do
121
+ is_expected.to match /^Berater:/
122
+ end
123
+
124
+ it 'combines the verb and path' do
125
+ is_expected.to match %r{:get:/$}
126
+ end
127
+ end
128
+
129
+ context 'with a different verb' do
130
+ let(:env) { Rack::MockRequest.env_for('/', method: 'PUT') }
131
+
132
+ it 'combines the verb and path' do
133
+ is_expected.to match %r{:put:/$}
134
+ end
135
+ end
136
+
137
+ context 'with a RESTful path' do
138
+ let(:env) { Rack::MockRequest.env_for('/user/123') }
139
+
140
+ it 'normalizes the id' do
141
+ is_expected.to match %r{:/user/x$}
142
+ end
143
+ end
144
+
145
+ context 'with a RESTful path and trailing slash' do
146
+ let(:env) { Rack::MockRequest.env_for('/user/123/') }
147
+
148
+ it 'normalizes the id and keeps the trailing slash' do
149
+ is_expected.to match %r{:/user/x/$}
150
+ end
151
+ end
152
+
153
+ context 'with a very RESTful path' do
154
+ let(:env) { Rack::MockRequest.env_for('/user/123/friend/456') }
155
+
156
+ it 'normalizes both ids' do
157
+ is_expected.to match %r{:/user/x/friend/x$}
158
+ end
159
+ end
160
+ end
161
+
162
+ context 'as Rack middleware' do
163
+ def call(path = '/')
164
+ get(path).body
165
+ end
166
+
167
+ let(:app) do
168
+ headers = {
169
+ 'Content-Type' => 'text/plain',
170
+ described_class::HEADER => app_priority,
171
+ }.compact
172
+
173
+ Rack::Builder.new do
174
+ use Rack::Lint
175
+ use Rack::Berater::Prioritizer
176
+
177
+ run (lambda do |env|
178
+ [200, headers, [ Rack::Berater::Prioritizer.current_priority.to_s ]]
179
+ end)
180
+ end
181
+ end
182
+
183
+ let(:app_priority) { nil }
184
+
185
+ it 'starts empty' do
186
+ expect(call).to be_empty
187
+ end
188
+
189
+ it 'parses incoming priority header' do
190
+ header described_class::HEADER, '7'
191
+
192
+ expect(call).to eq '7'
193
+ end
194
+
195
+ context 'when app returns a priority header' do
196
+ let(:app_priority) { '8' }
197
+
198
+ it 'parses the priority returned from the app' do
199
+ expect(call).to be_empty
200
+ expect(cache.values).to include app_priority
201
+ end
202
+
203
+ it 'uses the cached priority for subsequent calls' do
204
+ expect(call).to be_empty
205
+ expect(call).to eq app_priority
206
+ end
207
+ end
208
+
209
+ # context 'when two different endpoints are called' do
210
+ # fit 'parses and caches each priority' do
211
+ # @app_priority = '6'
212
+ # expect(call('/six')).to be_empty
213
+
214
+ # expect(call('/six')).to eq '6'
215
+
216
+ # @app_priority = '9'
217
+ # expect(call('/nine')).to be_empty
218
+ # expect(call('/nine')).to '9'
219
+ # end
220
+ # end
221
+ end
222
+
223
+ context 'as Rails middleware' do
224
+ before do
225
+ class EchoController < ActionController::Base
226
+ def index
227
+ render plain: Rack::Berater::Prioritizer.current_priority
228
+ end
229
+
230
+ def six
231
+ response.set_header(Rack::Berater::Prioritizer::HEADER, '6')
232
+ index
233
+ end
234
+
235
+ def nine
236
+ response.set_header(Rack::Berater::Prioritizer::HEADER, '9')
237
+ index
238
+ end
239
+ end
240
+
241
+ Rails.application = Class.new(Rails::Application) do
242
+ config.eager_load = false
243
+ config.hosts.clear # disable hostname filtering
244
+ end
245
+ Rails.application.middleware.use described_class
246
+ Rails.initialize!
247
+
248
+ Rails.application.routes.draw do
249
+ get '/' => 'echo#index'
250
+ get '/six' => 'echo#six'
251
+ get '/nine' => 'echo#nine'
252
+ end
253
+ end
254
+
255
+ let(:app) { Rails.application }
256
+ let(:middleware) { described_class.new(app) }
257
+
258
+ after { Rails.application = nil }
259
+
260
+ describe '#cache_key_for' do
261
+ subject { described_class.new(app).send(:cache_key_for, env) }
262
+
263
+ let(:env) { Rack::MockRequest.env_for('/') }
264
+
265
+ it 'uses the controller and action name' do
266
+ is_expected.to match %r{:echo#index$}
267
+ end
268
+ end
269
+
270
+ context 'when a priority header is sent' do
271
+ before { header described_class::HEADER, priority }
272
+
273
+ let(:priority) { '6' }
274
+
275
+ it 'sets the priority' do
276
+ expect(get('/six').body).to eq priority
277
+ end
278
+ end
279
+
280
+ context 'when the app returns a priority' do
281
+ it 'does not know the first time the controller is called' do
282
+ expect(get('/six').body).to be_empty
283
+ expect(get('/nine').body).to be_empty
284
+ end
285
+
286
+ it 'caches the repsonses for the second time' do
287
+ expect(get('/six').body).to be_empty
288
+ expect(get('/nine').body).to be_empty
289
+
290
+ expect(get('/six').body).to eq '6'
291
+ expect(get('/nine').body).to eq '9'
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails'
2
+ require 'rack/berater/railtie'
3
+
4
+ RSpec.describe Rack::Berater::Railtie do
5
+ subject { Rails.initialize! }
6
+
7
+ before do
8
+ Rails.application = Class.new(Rails::Application) do
9
+ config.eager_load = false
10
+ end
11
+ end
12
+
13
+ it 'adds middleware automatically' do
14
+ expect(subject.middleware).to include(Rack::Berater)
15
+ end
16
+ end
@@ -0,0 +1,155 @@
1
+ describe Rack::Berater do
2
+ let(:app) do
3
+ Rack::Builder.new do
4
+ use Rack::Lint
5
+ run (lambda do |env|
6
+ Berater::Unlimiter() do
7
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
8
+ end
9
+ end)
10
+ end
11
+ end
12
+ let(:response) { get '/' }
13
+
14
+ shared_examples 'works nominally' do
15
+ it 'has the correct status code' do
16
+ expect(response.status).to eq 200
17
+ end
18
+
19
+ it 'has the correct headers' do
20
+ expect(response.headers).to eq({
21
+ 'Content-Type' => 'text/plain',
22
+ 'Content-Length' => '2',
23
+ })
24
+ end
25
+
26
+ it 'has the correct body' do
27
+ expect(response.body).to eq 'OK'
28
+ end
29
+ end
30
+
31
+ context 'without middleware' do
32
+ include_examples 'works nominally'
33
+
34
+ it 'does not catch limit errors' do
35
+ Berater.test_mode = :fail
36
+ expect {
37
+ response
38
+ }.to be_overloaded
39
+ end
40
+ end
41
+
42
+ context 'with middleware using default settings' do
43
+ before { app.use described_class }
44
+
45
+ include_examples 'works nominally'
46
+
47
+ it 'catches and transforms limit errors' do
48
+ Berater.test_mode = :fail
49
+ expect(response.status).to eq 429
50
+ expect(response.body).to eq 'Too Many Requests'
51
+ end
52
+ end
53
+
54
+ context 'with middleware using custom settings' do
55
+ before do
56
+ app.use described_class, options
57
+ Berater.test_mode = :fail
58
+ end
59
+
60
+ context 'with a custom body' do
61
+ context 'with body nil' do
62
+ let(:options) { { body: nil } }
63
+
64
+ it 'falls back to the default' do
65
+ expect(response.body).to eq 'Too Many Requests'
66
+ end
67
+ end
68
+
69
+ context 'with body disabled' do
70
+ let(:options) { { body: false } }
71
+
72
+ it 'should not send a body' do
73
+ expect(response.body).to be_empty
74
+ end
75
+
76
+ it 'should not send the Content-Type header' do
77
+ expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
78
+ end
79
+ end
80
+
81
+ context 'with a string' do
82
+ let(:body) { 'none shall pass!' }
83
+ let(:options) { { body: body } }
84
+
85
+ it 'should send the custom string' do
86
+ expect(response.body).to eq body
87
+ end
88
+ end
89
+
90
+ context 'with an erroneous value' do
91
+ let(:options) { { body: 123 } }
92
+
93
+ it 'should raise an error' do
94
+ expect {
95
+ response
96
+ }.to raise_error(ArgumentError)
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'with custom headers' do
102
+ context 'with an extra header' do
103
+ let(:options) { { headers: { Rack::CACHE_CONTROL => 'no-cache' } } }
104
+
105
+ it 'should contain the default headers' do
106
+ expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
107
+ end
108
+
109
+ it 'should also contain the custom header' do
110
+ expect(response.headers).to include(options[:headers])
111
+ end
112
+ end
113
+
114
+ context 'with a new content type' do
115
+ let(:options) { { headers: { Rack::CONTENT_TYPE => 'application/json' } } }
116
+
117
+ it 'should override the Content-Type header' do
118
+ expect(response.headers).to include(options[:headers])
119
+ end
120
+ end
121
+ end
122
+
123
+ context 'with custom status code' do
124
+ let(:options) { { status_code: 503 } }
125
+
126
+ it 'catches and transforms limit errors' do
127
+ expect(response.status).to eq 503
128
+ expect(response.body).to eq 'Service Unavailable'
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'with custom error type' do
134
+ before do
135
+ app.use described_class
136
+ expect(Berater::Limiter).to receive(:new).and_raise(IOError)
137
+ end
138
+
139
+ it 'normally crashes the app' do
140
+ expect { response }.to raise_error(IOError)
141
+ end
142
+
143
+ context 'when an error type is registered with middleware' do
144
+ around do |example|
145
+ Rack::Berater::ERRORS << IOError
146
+ example.run
147
+ Rack::Berater::ERRORS.delete(IOError)
148
+ end
149
+
150
+ it 'catches and transforms limit errors' do
151
+ expect(response.status).to eq 429
152
+ end
153
+ end
154
+ end
155
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-12 00:00:00.000000000 Z
11
+ date: 2021-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: berater
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rspec
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +122,7 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
- description: "..."
125
+ description: limit incoming requests
112
126
  email:
113
127
  executables: []
114
128
  extensions: []
@@ -116,10 +130,13 @@ extra_rdoc_files: []
116
130
  files:
117
131
  - lib/rack-berater.rb
118
132
  - lib/rack/berater.rb
119
- - lib/rack/berater/handler.rb
133
+ - lib/rack/berater/prioritizer.rb
120
134
  - lib/rack/berater/railtie.rb
121
135
  - lib/rack/berater/version.rb
122
- - spec/handler_spec.rb
136
+ - spec/limiter_spec.rb
137
+ - spec/prioritizer_spec.rb
138
+ - spec/railtie_spec.rb
139
+ - spec/rescuer_spec.rb
123
140
  homepage: https://github.com/dpep/rack-berater
124
141
  licenses:
125
142
  - MIT
@@ -139,9 +156,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
156
  - !ruby/object:Gem::Version
140
157
  version: '0'
141
158
  requirements: []
142
- rubygems_version: 3.0.8
159
+ rubygems_version: 3.1.6
143
160
  signing_key:
144
161
  specification_version: 4
145
162
  summary: Rack::Berater
146
163
  test_files:
147
- - spec/handler_spec.rb
164
+ - spec/railtie_spec.rb
165
+ - spec/rescuer_spec.rb
166
+ - spec/prioritizer_spec.rb
167
+ - spec/limiter_spec.rb
@@ -1,37 +0,0 @@
1
- module Rack
2
- module Berater
3
- class Handler
4
- def initialize(app, options = {})
5
- @app = app
6
- @options = {
7
- status_code: options.fetch(:status_code, 429),
8
- body: options.fetch(:body, true),
9
- }
10
- end
11
-
12
- def call(env)
13
- @app.call(env)
14
- rescue ::Berater::Overloaded => e
15
- code = @options[:status_code]
16
- body = case @options[:body]
17
- when true
18
- Rack::Utils::HTTP_STATUS_CODES[code]
19
- when nil, false
20
- nil
21
- when String
22
- @options[:body]
23
- when Proc
24
- @options[:body].call(env, e)
25
- else
26
- raise ArgumentError, "invalid :body option: #{@options[:body]}"
27
- end
28
-
29
- [
30
- code,
31
- {},
32
- [ body ].compact,
33
- ]
34
- end
35
- end
36
- end
37
- end
data/spec/handler_spec.rb DELETED
@@ -1,98 +0,0 @@
1
- describe Rack::Berater::Handler do
2
- let(:app) do
3
- Rack::Builder.new do
4
- use Rack::Lint
5
- run (lambda do |env|
6
- raise ::Berater::Overloaded if ::Berater.test_mode == :fail
7
- [200, {"Content-Type" => "text/plain"}, ["OK"]]
8
- end)
9
- end
10
- end
11
- let(:response) { get "/" }
12
-
13
- shared_examples "works nominally" do
14
- it { expect(response.status).to eq 200 }
15
- it { expect(response.body).to eq "OK" }
16
- end
17
-
18
- context "without Handler" do
19
- include_examples "works nominally"
20
-
21
- it "does not catch limit errors" do
22
- Berater.test_mode = :fail
23
- expect {
24
- response
25
- }.to be_overloaded
26
- end
27
- end
28
-
29
- context "with Handler using default settings" do
30
- context "with default settings" do
31
- before { app.use described_class }
32
-
33
- include_examples "works nominally"
34
-
35
- it "catches and transforms limit errors" do
36
- Berater.test_mode = :fail
37
- expect(response.status).to eq 429
38
- expect(response.body).to eq "Too Many Requests"
39
- end
40
- end
41
- end
42
-
43
- context "with Handler using custom settings" do
44
- before do
45
- app.use described_class, options
46
- Berater.test_mode = :fail
47
- end
48
-
49
- context "with custom status code" do
50
- let(:options) { { status_code: 503 } }
51
-
52
- it "catches and transforms limit errors" do
53
- expect(response.status).to eq 503
54
- expect(response.body).to eq "Service Unavailable"
55
- end
56
- end
57
-
58
- context "with body disabled" do
59
- let(:options) { { body: false } }
60
-
61
- it "should not send a body" do
62
- expect(response.body).to be_empty
63
- end
64
- end
65
-
66
- context "with body nil" do
67
- let(:options) { { body: nil } }
68
-
69
- it "should not send a body" do
70
- expect(response.body).to be_empty
71
- end
72
- end
73
-
74
- context "with custom body" do
75
- let(:body) { "none shall pass!" }
76
- let(:options) { { body: body } }
77
-
78
- it "should send the custom string" do
79
- expect(response.body).to eq body
80
- end
81
- end
82
-
83
- context "with a dynamic body" do
84
- let(:body) { "none shall pass!" }
85
- let(:fn) { proc { body } }
86
- let(:options) { { body: fn } }
87
-
88
- it "should call the Proc and send the result" do
89
- expect(response.body).to eq body
90
- end
91
-
92
- it "should pass in the env and error" do
93
- expect(fn).to receive(:call).with(Hash, ::Berater::Overloaded)
94
- response
95
- end
96
- end
97
- end
98
- end