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 +4 -4
- data/lib/rack/berater/prioritizer.rb +93 -0
- data/lib/rack/berater/railtie.rb +6 -2
- data/lib/rack/berater/version.rb +2 -2
- data/lib/rack/berater.rb +63 -5
- data/lib/rack-berater.rb +1 -3
- data/spec/limiter_spec.rb +118 -0
- data/spec/prioritizer_spec.rb +295 -0
- data/spec/railtie_spec.rb +16 -0
- data/spec/rescuer_spec.rb +155 -0
- metadata +26 -6
- data/lib/rack/berater/handler.rb +0 -43
- data/spec/handler_spec.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b726fd0709ef54111d4e767fbe040ad3ab05fd0b0426bae20c67ef41f6fc869
|
4
|
+
data.tar.gz: 666a443527911b61660208f5e6ebf09b2a7adf298c67cb157665988c32868623
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/rack/berater/railtie.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
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,9 +1,67 @@
|
|
1
|
-
require
|
1
|
+
require 'berater'
|
2
|
+
require 'rack'
|
3
|
+
require 'rack/berater/version'
|
4
|
+
require 'set'
|
2
5
|
|
3
6
|
module Rack
|
4
|
-
|
5
|
-
autoload :
|
6
|
-
|
7
|
-
|
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
@@ -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.
|
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-
|
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/
|
133
|
+
- lib/rack/berater/prioritizer.rb
|
120
134
|
- lib/rack/berater/railtie.rb
|
121
135
|
- lib/rack/berater/version.rb
|
122
|
-
- spec/
|
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.
|
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/
|
164
|
+
- spec/railtie_spec.rb
|
165
|
+
- spec/rescuer_spec.rb
|
166
|
+
- spec/prioritizer_spec.rb
|
167
|
+
- spec/limiter_spec.rb
|
data/lib/rack/berater/handler.rb
DELETED
@@ -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
|