rack-berater 0.0.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5812fba3c2523be30274bcc192d67ba7cebbb1d2b52dcc0ca403d69c39630c
4
- data.tar.gz: 46152472d4af26a86e506a9150ff9c56743490707b630ecddc94beb0e63f7448
3
+ metadata.gz: 0b726fd0709ef54111d4e767fbe040ad3ab05fd0b0426bae20c67ef41f6fc869
4
+ data.tar.gz: 666a443527911b61660208f5e6ebf09b2a7adf298c67cb157665988c32868623
5
5
  SHA512:
6
- metadata.gz: '079407a6fd8d63c849809c972ef15739de09bd490c1e3a4046ae047dbba8b36cb7e5f0606c17b5e5a6a74ce8ec9fa95f61139afc3937a5c30643fe85ff59e91d'
7
- data.tar.gz: 7d6ede65719ed13181c25aca35a48c941ee9c4777caa4f56eede0552b4acdc39d2989069659217327a0b3bc494d0c178b1ee1faa21e42f5c70dd4067f4fb077e
6
+ metadata.gz: f5bfe591019a79137181785ae76f6e5603b3495cc5ef8e207eab1be88bec114988bbe190509752e093aa1458ecd2ce11dd91dcd4a31fe96b9c00e94f62a931ea
7
+ data.tar.gz: a83f26c06eeeb1c570233b1b84bdec1dadf501e8774a388290dd6c733fd2c095435f86974b4f27216ffbd118e261206097ac3f16187dfeb2f2da8eb03adb9657
@@ -0,0 +1,93 @@
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.respond_to?(:application) && Rails.application
59
+ res = Rails.application.routes.recognize_path(
60
+ env[Rack::PATH_INFO],
61
+ method: env[Rack::REQUEST_METHOD],
62
+ )
63
+ path = res.values_at(:controller, :action).compact.join('#')
64
+ end
65
+
66
+ if path.empty?
67
+ path = env['PATH_INFO'].gsub(%r{/[0-9]+(/|$)}, '/x\1')
68
+ end
69
+
70
+ [
71
+ @@cache_prefix,
72
+ req_method,
73
+ path,
74
+ ].join(':')
75
+ end
76
+
77
+ @@cache = {}
78
+
79
+ def cache_get(key)
80
+ synchronize { @@cache[key] }
81
+ end
82
+
83
+ def cache_set(key, priority)
84
+ synchronize { @@cache[key] = priority }
85
+ end
86
+
87
+ @@lock = Thread::Mutex.new
88
+ def synchronize(&block)
89
+ @@lock.synchronize(&block)
90
+ end
91
+ end
92
+ end
93
+ 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.2"
2
+ class Berater
3
+ VERSION = "0.3.1"
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
+ post '/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(post('/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(post('/nine').body).to be_empty
289
+
290
+ expect(get('/six').body).to eq '6'
291
+ expect(post('/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.2
4
+ version: 0.3.1
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-13 00:00:00.000000000 Z
11
+ date: 2021-11-18 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
@@ -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,43 +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
- headers: {
9
- Rack::CONTENT_TYPE => "text/plain",
10
- }.update(options.fetch(:headers, {})),
11
- body: options.fetch(:body, true),
12
- }
13
- end
14
-
15
- def call(env)
16
- @app.call(env)
17
- rescue ::Berater::Overloaded => e
18
- code = @options[:status_code]
19
-
20
- body = case @options[:body]
21
- when true
22
- Rack::Utils::HTTP_STATUS_CODES[code]
23
- when nil, false
24
- nil
25
- when String
26
- @options[:body]
27
- when Proc
28
- @options[:body].call(env, e)
29
- else
30
- raise ArgumentError, "invalid :body option: #{@options[:body]}"
31
- end
32
-
33
- headers = body ? @options[:headers] : {}
34
-
35
- [
36
- code,
37
- headers,
38
- [ body ].compact,
39
- ]
40
- end
41
- end
42
- end
43
- end
data/spec/handler_spec.rb DELETED
@@ -1,126 +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 "has the correct status code" do
15
- expect(response.status).to eq 200
16
- end
17
-
18
- it "has the correct headers" do
19
- expect(response.headers).to eq({
20
- "Content-Type" => "text/plain",
21
- "Content-Length" => "2",
22
- })
23
- end
24
-
25
- it "has the correct body" do
26
- expect(response.body).to eq "OK"
27
- end
28
- end
29
-
30
- context "without Handler" do
31
- include_examples "works nominally"
32
-
33
- it "does not catch limit errors" do
34
- Berater.test_mode = :fail
35
- expect {
36
- response
37
- }.to be_overloaded
38
- end
39
- end
40
-
41
- context "with Handler using default settings" do
42
- context "with 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
- end
54
-
55
- context "with Handler using custom settings" do
56
- before do
57
- app.use described_class, options
58
- Berater.test_mode = :fail
59
- end
60
-
61
- context "with custom status code" do
62
- let(:options) { { status_code: 503 } }
63
-
64
- it "catches and transforms limit errors" do
65
- expect(response.status).to eq 503
66
- expect(response.body).to eq "Service Unavailable"
67
- end
68
- end
69
-
70
- context "with body disabled" do
71
- let(:options) { { body: false } }
72
-
73
- it "should not send a body" do
74
- expect(response.body).to be_empty
75
- end
76
-
77
- it "should not send the Content-Type header" do
78
- expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
79
- end
80
- end
81
-
82
- context "with body nil" do
83
- let(:options) { { body: nil } }
84
-
85
- it "should not send a body" do
86
- expect(response.body).to be_empty
87
- end
88
- end
89
-
90
- context "with custom body" do
91
- let(:body) { "none shall pass!" }
92
- let(:options) { { body: body } }
93
-
94
- it "should send the custom string" do
95
- expect(response.body).to eq body
96
- end
97
- end
98
-
99
- context "with a dynamic body" do
100
- let(:body) { "none shall pass!" }
101
- let(:fn) { proc { body } }
102
- let(:options) { { body: fn } }
103
-
104
- it "should call the Proc and send the result" do
105
- expect(response.body).to eq body
106
- end
107
-
108
- it "should pass in the env and error" do
109
- expect(fn).to receive(:call).with(Hash, ::Berater::Overloaded)
110
- response
111
- end
112
- end
113
-
114
- context "with custom headers" do
115
- let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
116
-
117
- it "should contain the default headers" do
118
- expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
119
- end
120
-
121
- it "should also contain custom header" do
122
- expect(response.headers).to include(options[:headers])
123
- end
124
- end
125
- end
126
- end