rack-berater 0.0.2 → 0.1.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/railtie.rb +1 -1
- data/lib/rack/berater/version.rb +2 -2
- data/lib/rack/berater.rb +55 -3
- data/lib/rack-berater.rb +0 -2
- data/spec/limiter_spec.rb +96 -0
- data/spec/rescuer_spec.rb +145 -0
- metadata +7 -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: 3475519672f1074d749de982b36d3a2d0d15a468d38d35b91e0f237de02ba2f8
|
4
|
+
data.tar.gz: fd08c05ccc694adc8ca540a2468956144c0d180611dd4622070e15aa8dd120ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88428422463a296369f8a2f141c74dadc90f132e2d2072c3505bf33012e344668277e23146125cf43bca55a7d6ec83da0871d6e5b23684a72f3875e48f5d43d7
|
7
|
+
data.tar.gz: be42f6faa19469ccbfed7995f02b81533051b828e688779d8b5986be6a83174baf71c8ca399b9fabb419766a59fbd8a0c366eefdee977865d7c215970e815527
|
data/lib/rack/berater/railtie.rb
CHANGED
data/lib/rack/berater/version.rb
CHANGED
data/lib/rack/berater.rb
CHANGED
@@ -1,9 +1,61 @@
|
|
1
|
+
require "berater"
|
2
|
+
require "rack"
|
1
3
|
require "rack/berater/version"
|
4
|
+
require "set"
|
2
5
|
|
3
6
|
module Rack
|
4
|
-
|
5
|
-
autoload :Handler, "rack/berater/handler"
|
6
|
-
# autoload :Limiter, "rack/berater/limiter"
|
7
|
+
class Berater
|
7
8
|
autoload :Railtie, "rack/berater/railtie"
|
9
|
+
|
10
|
+
ERROR_TYPES = Set.new([ ::Berater::Overloaded ])
|
11
|
+
|
12
|
+
def initialize(app, options = {})
|
13
|
+
@app = app
|
14
|
+
@enabled = options[:enabled?]
|
15
|
+
@limiter = options[:limiter]
|
16
|
+
@options = {
|
17
|
+
headers: {},
|
18
|
+
status_code: options.fetch(:status_code, 429),
|
19
|
+
}
|
20
|
+
|
21
|
+
# configure body
|
22
|
+
@options[:body] = case options[:body]
|
23
|
+
when true, nil
|
24
|
+
Rack::Utils::HTTP_STATUS_CODES[@options[:status_code]]
|
25
|
+
when false
|
26
|
+
nil
|
27
|
+
when String
|
28
|
+
options[:body]
|
29
|
+
else
|
30
|
+
raise ArgumentError, "invalid :body option: #{options[:body]}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# configure headers
|
34
|
+
if @options[:body]
|
35
|
+
@options[:headers][Rack::CONTENT_TYPE] = "text/plain"
|
36
|
+
end
|
37
|
+
@options[:headers].update(options.fetch(:headers, {}))
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
if enabled?(env)
|
42
|
+
@limiter.limit { @app.call(env) }
|
43
|
+
else
|
44
|
+
@app.call(env)
|
45
|
+
end
|
46
|
+
rescue *ERROR_TYPES => e
|
47
|
+
[
|
48
|
+
@options[:status_code],
|
49
|
+
@options[:headers],
|
50
|
+
[ @options[:body] ].compact,
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def enabled?(env)
|
57
|
+
return false unless @limiter
|
58
|
+
@enabled.nil? ? true : @enabled.call(env)
|
59
|
+
end
|
8
60
|
end
|
9
61
|
end
|
data/lib/rack-berater.rb
CHANGED
@@ -0,0 +1,96 @@
|
|
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
|
+
let(:limiter) { ::Berater::Unlimiter.new }
|
43
|
+
|
44
|
+
include_examples "works nominally"
|
45
|
+
|
46
|
+
it "calls the limiter" do
|
47
|
+
expect(limiter).to receive(:limit).and_call_original
|
48
|
+
response
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when operating beyond limits" do
|
52
|
+
before { Berater.test_mode = :fail }
|
53
|
+
|
54
|
+
it "returns an error" do
|
55
|
+
expect(response.status).to eq 429
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "enabled? option" do
|
61
|
+
after { expect(response.status).to eq 200 }
|
62
|
+
|
63
|
+
let(:enabled?) { double }
|
64
|
+
|
65
|
+
context "when there is a limiter" do
|
66
|
+
let(:limiter) { ::Berater::Unlimiter.new }
|
67
|
+
|
68
|
+
it "should be called with the env hash" do
|
69
|
+
expect(enabled?).to receive(:call) do |env|
|
70
|
+
expect(env).to be_a Hash
|
71
|
+
expect(Rack::Request.new(env).path).to eq "/"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "when enabled" do
|
76
|
+
it "should call the limiter" do
|
77
|
+
expect(enabled?).to receive(:call).and_return(true)
|
78
|
+
expect(limiter).to receive(:limit).and_call_original
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when disabled" do
|
83
|
+
it "should not call the limiter" do
|
84
|
+
expect(enabled?).to receive(:call).and_return(false)
|
85
|
+
expect(limiter).not_to receive(:limit)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when there is no limiter" do
|
91
|
+
it "should not call enabled?" do
|
92
|
+
expect(enabled?).not_to receive(:call)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,145 @@
|
|
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 "should default to sending a body" 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
|
+
end
|
90
|
+
|
91
|
+
context "with custom headers" do
|
92
|
+
context "with an extra header" do
|
93
|
+
let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
|
94
|
+
|
95
|
+
it "should contain the default headers" do
|
96
|
+
expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should also contain the custom header" do
|
100
|
+
expect(response.headers).to include(options[:headers])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "with a new content type" do
|
105
|
+
let(:options) { { headers: { Rack::CONTENT_TYPE => "application/json" } } }
|
106
|
+
|
107
|
+
it "should override the Content-Type header" do
|
108
|
+
expect(response.headers).to include(options[:headers])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "with custom status code" do
|
114
|
+
let(:options) { { status_code: 503 } }
|
115
|
+
|
116
|
+
it "catches and transforms limit errors" do
|
117
|
+
expect(response.status).to eq 503
|
118
|
+
expect(response.body).to eq "Service Unavailable"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "with custom error type" do
|
124
|
+
before do
|
125
|
+
app.use described_class
|
126
|
+
expect(Berater::Limiter).to receive(:new).and_raise(IOError)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "normally crashes the app" do
|
130
|
+
expect { response }.to raise_error(IOError)
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when an error type is registered with middleware" do
|
134
|
+
around do |example|
|
135
|
+
Rack::Berater::ERROR_TYPES << IOError
|
136
|
+
example.run
|
137
|
+
Rack::Berater::ERROR_TYPES.delete(IOError)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "catches and transforms limit errors" do
|
141
|
+
expect(response.status).to eq 429
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
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
|
4
|
+
version: 0.1.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-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: berater
|
@@ -116,10 +116,10 @@ extra_rdoc_files: []
|
|
116
116
|
files:
|
117
117
|
- lib/rack-berater.rb
|
118
118
|
- lib/rack/berater.rb
|
119
|
-
- lib/rack/berater/handler.rb
|
120
119
|
- lib/rack/berater/railtie.rb
|
121
120
|
- lib/rack/berater/version.rb
|
122
|
-
- spec/
|
121
|
+
- spec/limiter_spec.rb
|
122
|
+
- spec/rescuer_spec.rb
|
123
123
|
homepage: https://github.com/dpep/rack-berater
|
124
124
|
licenses:
|
125
125
|
- MIT
|
@@ -139,9 +139,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
139
|
- !ruby/object:Gem::Version
|
140
140
|
version: '0'
|
141
141
|
requirements: []
|
142
|
-
rubygems_version: 3.
|
142
|
+
rubygems_version: 3.1.4
|
143
143
|
signing_key:
|
144
144
|
specification_version: 4
|
145
145
|
summary: Rack::Berater
|
146
146
|
test_files:
|
147
|
-
- spec/
|
147
|
+
- spec/rescuer_spec.rb
|
148
|
+
- 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
|