rack-berater 0.2.0 → 0.3.0
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 +4 -4
- data/lib/rack/berater/prioritizer.rb +90 -0
- data/lib/rack/berater/railtie.rb +6 -2
- data/lib/rack/berater/version.rb +1 -1
- data/lib/rack/berater.rb +8 -7
- data/lib/rack-berater.rb +1 -1
- data/spec/limiter_spec.rb +32 -32
- data/spec/prioritizer_spec.rb +295 -0
- data/spec/railtie_spec.rb +3 -4
- data/spec/rescuer_spec.rb +43 -43
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5840eaff6319d5e24c78c2f50960138ca27fcce25bb3d45354e7eca195b50f0d
|
4
|
+
data.tar.gz: f9cdde9f38c97d82e03d6a1e9cea2e1939eaca13f4202a2a674a6fda4e6f530e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/rack/berater/railtie.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
require
|
1
|
+
require 'rails/railtie'
|
2
2
|
|
3
3
|
module Rack
|
4
4
|
class Berater
|
5
5
|
class Railtie < Rails::Railtie
|
6
|
-
initializer
|
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
|
data/lib/rack/berater/version.rb
CHANGED
data/lib/rack/berater.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'berater'
|
2
|
+
require 'rack'
|
3
|
+
require 'rack/berater/version'
|
4
|
+
require 'set'
|
5
5
|
|
6
6
|
module Rack
|
7
7
|
class Berater
|
8
|
-
autoload :
|
8
|
+
autoload :Prioritizer, 'rack/berater/prioritizer'
|
9
|
+
autoload :Railtie, 'rack/berater/railtie'
|
9
10
|
|
10
11
|
ERRORS = Set[ ::Berater::Overloaded ]
|
11
12
|
|
@@ -27,12 +28,12 @@ module Rack
|
|
27
28
|
when String
|
28
29
|
options[:body]
|
29
30
|
else
|
30
|
-
raise ArgumentError,
|
31
|
+
raise ArgumentError, 'invalid :body option: #{options[:body]}'
|
31
32
|
end
|
32
33
|
|
33
34
|
# configure headers
|
34
35
|
if @options[:body]
|
35
|
-
@options[:headers][Rack::CONTENT_TYPE] =
|
36
|
+
@options[:headers][Rack::CONTENT_TYPE] = 'text/plain'
|
36
37
|
end
|
37
38
|
@options[:headers].update(options.fetch(:headers, {}))
|
38
39
|
end
|
data/lib/rack-berater.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'rack/berater'
|
data/spec/limiter_spec.rb
CHANGED
@@ -9,108 +9,108 @@ describe Rack::Berater do
|
|
9
9
|
Rack::Builder.new do
|
10
10
|
use Rack::Lint
|
11
11
|
run (lambda do |env|
|
12
|
-
[200, {
|
12
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
13
13
|
end)
|
14
14
|
end
|
15
15
|
end
|
16
|
-
let(:response) { get
|
16
|
+
let(:response) { get '/' }
|
17
17
|
|
18
|
-
shared_examples
|
19
|
-
it
|
18
|
+
shared_examples 'works nominally' do
|
19
|
+
it 'has the correct status code' do
|
20
20
|
expect(response.status).to eq 200
|
21
21
|
end
|
22
22
|
|
23
|
-
it
|
23
|
+
it 'has the correct headers' do
|
24
24
|
expect(response.headers).to eq({
|
25
|
-
|
26
|
-
|
25
|
+
'Content-Type' => 'text/plain',
|
26
|
+
'Content-Length' => '2',
|
27
27
|
})
|
28
28
|
end
|
29
29
|
|
30
|
-
it
|
31
|
-
expect(response.body).to eq
|
30
|
+
it 'has the correct body' do
|
31
|
+
expect(response.body).to eq 'OK'
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
context
|
35
|
+
context 'without a limiter' do
|
36
36
|
before { Berater.test_mode = :fail }
|
37
37
|
|
38
|
-
include_examples
|
38
|
+
include_examples 'works nominally'
|
39
39
|
end
|
40
40
|
|
41
|
-
describe
|
42
|
-
context
|
41
|
+
describe 'limiter option' do
|
42
|
+
context 'when limiter is a limiter' do
|
43
43
|
let(:limiter) { ::Berater::Unlimiter.new }
|
44
44
|
|
45
|
-
include_examples
|
45
|
+
include_examples 'works nominally'
|
46
46
|
|
47
|
-
it
|
47
|
+
it 'calls the limiter' do
|
48
48
|
expect(limiter).to receive(:limit).and_call_original
|
49
49
|
response
|
50
50
|
end
|
51
51
|
|
52
|
-
context
|
52
|
+
context 'when operating beyond limits' do
|
53
53
|
before { Berater.test_mode = :fail }
|
54
54
|
|
55
|
-
it
|
55
|
+
it 'returns an error' do
|
56
56
|
expect(response.status).to eq 429
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
context
|
61
|
+
context 'when limiter is a proc' do
|
62
62
|
let(:limiter_instance) { ::Berater::Unlimiter.new }
|
63
63
|
let(:limiter) { Proc.new { limiter_instance } }
|
64
64
|
|
65
|
-
include_examples
|
65
|
+
include_examples 'works nominally'
|
66
66
|
|
67
|
-
it
|
67
|
+
it 'calls the proc with env' do
|
68
68
|
expect(limiter).to receive(:call).with(Hash).and_call_original
|
69
69
|
response
|
70
70
|
end
|
71
71
|
|
72
|
-
context
|
72
|
+
context 'when operating beyond limits' do
|
73
73
|
before { Berater.test_mode = :fail }
|
74
74
|
|
75
|
-
it
|
75
|
+
it 'returns an error' do
|
76
76
|
expect(response.status).to eq 429
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
-
describe
|
82
|
+
describe 'enabled? option' do
|
83
83
|
after { expect(response.status).to eq 200 }
|
84
84
|
|
85
85
|
let(:enabled?) { double }
|
86
86
|
|
87
|
-
context
|
87
|
+
context 'when there is a limiter' do
|
88
88
|
let(:limiter) { ::Berater::Unlimiter.new }
|
89
89
|
|
90
|
-
it
|
90
|
+
it 'should be called with the env hash' do
|
91
91
|
expect(enabled?).to receive(:call) do |env|
|
92
92
|
expect(env).to be_a Hash
|
93
|
-
expect(Rack::Request.new(env).path).to eq
|
93
|
+
expect(Rack::Request.new(env).path).to eq '/'
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
-
context
|
98
|
-
it
|
97
|
+
context 'when enabled' do
|
98
|
+
it 'should call the limiter' do
|
99
99
|
expect(enabled?).to receive(:call).and_return(true)
|
100
100
|
expect(limiter).to receive(:limit).and_call_original
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
context
|
105
|
-
it
|
104
|
+
context 'when disabled' do
|
105
|
+
it 'should not call the limiter' do
|
106
106
|
expect(enabled?).to receive(:call).and_return(false)
|
107
107
|
expect(limiter).not_to receive(:limit)
|
108
108
|
end
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
-
context
|
113
|
-
it
|
112
|
+
context 'when there is no limiter' do
|
113
|
+
it 'should not call enabled?' do
|
114
114
|
expect(enabled?).not_to receive(:call)
|
115
115
|
end
|
116
116
|
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
|
data/spec/railtie_spec.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'rails'
|
2
|
+
require 'rack/berater/railtie'
|
3
3
|
|
4
4
|
RSpec.describe Rack::Berater::Railtie do
|
5
5
|
subject { Rails.initialize! }
|
@@ -7,11 +7,10 @@ RSpec.describe Rack::Berater::Railtie do
|
|
7
7
|
before do
|
8
8
|
Rails.application = Class.new(Rails::Application) do
|
9
9
|
config.eager_load = false
|
10
|
-
config.logger = ActiveSupport::Logger.new($stdout)
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
14
|
-
it
|
13
|
+
it 'adds middleware automatically' do
|
15
14
|
expect(subject.middleware).to include(Rack::Berater)
|
16
15
|
end
|
17
16
|
end
|
data/spec/rescuer_spec.rb
CHANGED
@@ -4,34 +4,34 @@ describe Rack::Berater do
|
|
4
4
|
use Rack::Lint
|
5
5
|
run (lambda do |env|
|
6
6
|
Berater::Unlimiter() do
|
7
|
-
[200, {
|
7
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
8
8
|
end
|
9
9
|
end)
|
10
10
|
end
|
11
11
|
end
|
12
|
-
let(:response) { get
|
12
|
+
let(:response) { get '/' }
|
13
13
|
|
14
|
-
shared_examples
|
15
|
-
it
|
14
|
+
shared_examples 'works nominally' do
|
15
|
+
it 'has the correct status code' do
|
16
16
|
expect(response.status).to eq 200
|
17
17
|
end
|
18
18
|
|
19
|
-
it
|
19
|
+
it 'has the correct headers' do
|
20
20
|
expect(response.headers).to eq({
|
21
|
-
|
22
|
-
|
21
|
+
'Content-Type' => 'text/plain',
|
22
|
+
'Content-Length' => '2',
|
23
23
|
})
|
24
24
|
end
|
25
25
|
|
26
|
-
it
|
27
|
-
expect(response.body).to eq
|
26
|
+
it 'has the correct body' do
|
27
|
+
expect(response.body).to eq 'OK'
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
context
|
32
|
-
include_examples
|
31
|
+
context 'without middleware' do
|
32
|
+
include_examples 'works nominally'
|
33
33
|
|
34
|
-
it
|
34
|
+
it 'does not catch limit errors' do
|
35
35
|
Berater.test_mode = :fail
|
36
36
|
expect {
|
37
37
|
response
|
@@ -39,55 +39,55 @@ describe Rack::Berater do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
context
|
42
|
+
context 'with middleware using default settings' do
|
43
43
|
before { app.use described_class }
|
44
44
|
|
45
|
-
include_examples
|
45
|
+
include_examples 'works nominally'
|
46
46
|
|
47
|
-
it
|
47
|
+
it 'catches and transforms limit errors' do
|
48
48
|
Berater.test_mode = :fail
|
49
49
|
expect(response.status).to eq 429
|
50
|
-
expect(response.body).to eq
|
50
|
+
expect(response.body).to eq 'Too Many Requests'
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
context
|
54
|
+
context 'with middleware using custom settings' do
|
55
55
|
before do
|
56
56
|
app.use described_class, options
|
57
57
|
Berater.test_mode = :fail
|
58
58
|
end
|
59
59
|
|
60
|
-
context
|
61
|
-
context
|
60
|
+
context 'with a custom body' do
|
61
|
+
context 'with body nil' do
|
62
62
|
let(:options) { { body: nil } }
|
63
63
|
|
64
|
-
it
|
65
|
-
expect(response.body).to eq
|
64
|
+
it 'falls back to the default' do
|
65
|
+
expect(response.body).to eq 'Too Many Requests'
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
-
context
|
69
|
+
context 'with body disabled' do
|
70
70
|
let(:options) { { body: false } }
|
71
71
|
|
72
|
-
it
|
72
|
+
it 'should not send a body' do
|
73
73
|
expect(response.body).to be_empty
|
74
74
|
end
|
75
75
|
|
76
|
-
it
|
76
|
+
it 'should not send the Content-Type header' do
|
77
77
|
expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
context
|
82
|
-
let(:body) {
|
81
|
+
context 'with a string' do
|
82
|
+
let(:body) { 'none shall pass!' }
|
83
83
|
let(:options) { { body: body } }
|
84
84
|
|
85
|
-
it
|
85
|
+
it 'should send the custom string' do
|
86
86
|
expect(response.body).to eq body
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
|
-
context
|
90
|
+
context 'with an erroneous value' do
|
91
91
|
let(:options) { { body: 123 } }
|
92
92
|
|
93
93
|
it 'should raise an error' do
|
@@ -98,56 +98,56 @@ describe Rack::Berater do
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
101
|
-
context
|
102
|
-
context
|
103
|
-
let(:options) { { headers: { Rack::CACHE_CONTROL =>
|
101
|
+
context 'with custom headers' do
|
102
|
+
context 'with an extra header' do
|
103
|
+
let(:options) { { headers: { Rack::CACHE_CONTROL => 'no-cache' } } }
|
104
104
|
|
105
|
-
it
|
105
|
+
it 'should contain the default headers' do
|
106
106
|
expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
|
107
107
|
end
|
108
108
|
|
109
|
-
it
|
109
|
+
it 'should also contain the custom header' do
|
110
110
|
expect(response.headers).to include(options[:headers])
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
-
context
|
115
|
-
let(:options) { { headers: { Rack::CONTENT_TYPE =>
|
114
|
+
context 'with a new content type' do
|
115
|
+
let(:options) { { headers: { Rack::CONTENT_TYPE => 'application/json' } } }
|
116
116
|
|
117
|
-
it
|
117
|
+
it 'should override the Content-Type header' do
|
118
118
|
expect(response.headers).to include(options[:headers])
|
119
119
|
end
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
-
context
|
123
|
+
context 'with custom status code' do
|
124
124
|
let(:options) { { status_code: 503 } }
|
125
125
|
|
126
|
-
it
|
126
|
+
it 'catches and transforms limit errors' do
|
127
127
|
expect(response.status).to eq 503
|
128
|
-
expect(response.body).to eq
|
128
|
+
expect(response.body).to eq 'Service Unavailable'
|
129
129
|
end
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
|
-
context
|
133
|
+
context 'with custom error type' do
|
134
134
|
before do
|
135
135
|
app.use described_class
|
136
136
|
expect(Berater::Limiter).to receive(:new).and_raise(IOError)
|
137
137
|
end
|
138
138
|
|
139
|
-
it
|
139
|
+
it 'normally crashes the app' do
|
140
140
|
expect { response }.to raise_error(IOError)
|
141
141
|
end
|
142
142
|
|
143
|
-
context
|
143
|
+
context 'when an error type is registered with middleware' do
|
144
144
|
around do |example|
|
145
145
|
Rack::Berater::ERRORS << IOError
|
146
146
|
example.run
|
147
147
|
Rack::Berater::ERRORS.delete(IOError)
|
148
148
|
end
|
149
149
|
|
150
|
-
it
|
150
|
+
it 'catches and transforms limit errors' do
|
151
151
|
expect(response.status).to eq 429
|
152
152
|
end
|
153
153
|
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.
|
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-
|
11
|
+
date: 2021-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: berater
|
@@ -130,9 +130,11 @@ extra_rdoc_files: []
|
|
130
130
|
files:
|
131
131
|
- lib/rack-berater.rb
|
132
132
|
- lib/rack/berater.rb
|
133
|
+
- lib/rack/berater/prioritizer.rb
|
133
134
|
- lib/rack/berater/railtie.rb
|
134
135
|
- lib/rack/berater/version.rb
|
135
136
|
- spec/limiter_spec.rb
|
137
|
+
- spec/prioritizer_spec.rb
|
136
138
|
- spec/railtie_spec.rb
|
137
139
|
- spec/rescuer_spec.rb
|
138
140
|
homepage: https://github.com/dpep/rack-berater
|
@@ -161,4 +163,5 @@ summary: Rack::Berater
|
|
161
163
|
test_files:
|
162
164
|
- spec/railtie_spec.rb
|
163
165
|
- spec/rescuer_spec.rb
|
166
|
+
- spec/prioritizer_spec.rb
|
164
167
|
- spec/limiter_spec.rb
|