rack-robustness 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46cc2e7cca1b36e536f5e2e26a5e48c93e793b4c144944001464120fd6b01afa
4
+ data.tar.gz: 625f614030368429b7a2cd9290adfdab5ca1ec668784df94214b7a16528c7663
5
+ SHA512:
6
+ metadata.gz: 6306779c8ba88fd038e9184604a995bd4694ce3f4d98428fbf2013a71f4cb8ede0d386df8b967c442baf7735f87a84773aab910b5e2b29b607182f90459042f6
7
+ data.tar.gz: 8dde80698d33fdc1bde9c605f69dfba631bbbe0649084d6c5b4a810f66bf9c66c818f81e56d7aab87846f0ef9a44122aad19e1702728d8cbec7d96d6a28b841f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,58 @@
1
- # 1.0.0 / 2013-02-26
1
+ ## 1.2.0 / 2023-06-09
2
+
3
+ * Modernize with test matrix on ruby 2.7, 3.1, and 3.2
4
+
5
+ * Fix usage of Fixnum to be compatible with Ruby 3.x
6
+
7
+ ## 1.1.0 / 2013-04-16
8
+
9
+ * Fixed catching of non standard errors (e.g. SecurityError)
10
+
11
+ * Global headers are now correctly overrided by specific per-exception headers
12
+
13
+ * Renamed `#on` as `#rescue` for better capturing semantics of `on` blocks (now an alias).
14
+
15
+ * Added last resort exception handling if an error occurs during exception handling itself.
16
+ In `no_catch_all` mode, the exception is simply reraised; otherwise a default 500 error
17
+ is returned with a safe message.
18
+
19
+ * Added a shortcut form for `#rescue` clauses allowing values directly, e.g.,
20
+
21
+ use Rack::Robustness do |g|
22
+ g.rescue(SecurityError, 403)
23
+ end
24
+
25
+ * Added suppport for ensure clause(s), always called after `rescue` blocks
26
+
27
+ * Rack's `env` is now available in all error handling blocks, e.g.,
28
+
29
+ use Rack::Robustness do |g|
30
+ g.status{|ex| ... env ... }
31
+ g.body {|ex| ... env ... }
32
+ g.rescue(SecurityError){|ex| ... env ... }
33
+ g.ensure{|ex| ... env ... }
34
+ end
35
+
36
+ * Similarly, Rack::Robustness now internally uses instances of Rack::Request and Rack::Response;
37
+ `request` and `response` are available in all blocks. The specific Response
38
+ object to use can be built using the `response` DSL method, e.g.,
39
+
40
+ use Rack::Robustness do |g|
41
+ g.response{|ex| MyOwnRackResponse.new }
42
+ end
43
+
44
+ * Rack::Robustness may now be subclassed as an alternative to inline `use`, e.g.
45
+
46
+ class Shield < Rack::Robustness
47
+ self.body {|ex| ... }
48
+ self.rescue(SecurityError){|ex| ... }
49
+ ...
50
+ end
51
+
52
+ # in Rack-based configuration
53
+ use Shield
54
+
55
+ ## 1.0.0 / 2013-02-26
2
56
 
3
57
  * Enhancements
4
58
 
data/Gemfile CHANGED
@@ -1,8 +1,8 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
3
  group :development do
4
- gem "rack", "~> 1.5"
5
- gem "rake", "~> 10.0"
6
- gem "rspec", "~> 2.12"
4
+ gem "rack", "~> 2"
5
+ gem "rake", "~> 13"
6
+ gem "rspec", "~> 3"
7
7
  gem "rack-test", "~> 0.6"
8
8
  end
data/Gemfile.lock CHANGED
@@ -1,25 +1,33 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- diff-lcs (1.1.3)
5
- rack (1.5.2)
4
+ diff-lcs (1.5.0)
5
+ rack (2.2.7)
6
6
  rack-test (0.6.2)
7
7
  rack (>= 1.0)
8
- rake (10.0.3)
9
- rspec (2.12.0)
10
- rspec-core (~> 2.12.0)
11
- rspec-expectations (~> 2.12.0)
12
- rspec-mocks (~> 2.12.0)
13
- rspec-core (2.12.2)
14
- rspec-expectations (2.12.1)
15
- diff-lcs (~> 1.1.3)
16
- rspec-mocks (2.12.2)
8
+ rake (13.0.6)
9
+ rspec (3.12.0)
10
+ rspec-core (~> 3.12.0)
11
+ rspec-expectations (~> 3.12.0)
12
+ rspec-mocks (~> 3.12.0)
13
+ rspec-core (3.12.2)
14
+ rspec-support (~> 3.12.0)
15
+ rspec-expectations (3.12.3)
16
+ diff-lcs (>= 1.2.0, < 2.0)
17
+ rspec-support (~> 3.12.0)
18
+ rspec-mocks (3.12.5)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.12.0)
21
+ rspec-support (3.12.0)
17
22
 
18
23
  PLATFORMS
19
24
  ruby
20
25
 
21
26
  DEPENDENCIES
22
- rack (~> 1.5)
27
+ rack (~> 2)
23
28
  rack-test (~> 0.6)
24
- rake (~> 10.0)
25
- rspec (~> 2.12)
29
+ rake (~> 13)
30
+ rspec (~> 3)
31
+
32
+ BUNDLED WITH
33
+ 2.4.6
data/README.md CHANGED
@@ -1,15 +1,59 @@
1
1
  # Rack::Robustness, the rescue clause of your Rack stack.
2
2
 
3
- Rack::Robustness is the rescue clause of your Rack's call stack. In other words, a middleware that ensures the robustness of your web stack, because exceptions occur either intentionally or unintentionally. It scales from zero configuration (a default shield) to specific rescue clauses for specific errors.
3
+ Rack::Robustness is the rescue clause of your Rack's call stack. In other words, a middleware that ensures the robustness of your web stack, because exceptions occur either intentionally or unintentionally. Rack::Robustness is the rack middleware you would have written manually (see below) but provides a DSL for scaling from zero configuration (a default shield) to specific rescue clauses for specific errors.
4
4
 
5
5
  [![Build Status](https://secure.travis-ci.org/blambeau/rack-robustness.png)](http://travis-ci.org/blambeau/rack-robustness)
6
6
  [![Dependency Status](https://gemnasium.com/blambeau/rack-robustness.png)](https://gemnasium.com/blambeau/rack-robustness)
7
7
 
8
+ ```ruby
9
+ ##
10
+ #
11
+ # The middleware you would have written
12
+ #
13
+ class Robustness
14
+
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ @app.call(env)
21
+ rescue ArgumentError => ex
22
+ [400, { 'Content-Type' => 'text/plain' }, [ ex.message ] ] # suppose the message can be safely used
23
+ rescue SecurityError => ex
24
+ [403, { 'Content-Type' => 'text/plain' }, [ ex.message ] ]
25
+ ensure
26
+ env['rack.errors'].write(ex.message) if ex
27
+ end
28
+
29
+ end
30
+ ```
31
+
32
+ ...becomes...
33
+
34
+ ```ruby
35
+ use Rack::Robustness do |g|
36
+ g.on(ArgumentError){|ex| 400 }
37
+ g.on(SecurityError){|ex| 403 }
38
+
39
+ g.content_type 'text/plain'
40
+
41
+ g.body{|ex|
42
+ ex.message
43
+ }
44
+
45
+ g.ensure(true){|ex|
46
+ env['rack.errors'].write(ex.message)
47
+ }
48
+ end
49
+ ```
50
+
8
51
  ## Links
9
52
 
10
- https://github.com/blambeau/rack-robustness
53
+ * https://github.com/blambeau/rack-robustness
54
+ * http://www.revision-zero.org/rack-robustness
11
55
 
12
- ## Why?
56
+ ## Why?
13
57
 
14
58
  In my opinion, Sinatra's error handling is sometimes a bit limited for real-case needs. So I came up with something a bit more Rack-ish, that allows handling exceptions actively, because exceptions occur and that you'll handle them... enventually. A more theoretic argumentation would be:
15
59
 
@@ -17,20 +61,22 @@ In my opinion, Sinatra's error handling is sometimes a bit limited for real-case
17
61
  * The behavior to adopt when obstacles occur is not necessary defined where the exception is thrown, but often higher in the call stack.
18
62
  * In ruby web apps, the Rack's call stack is a very important part of your stack. Middlewares, routes and controllers do rarely rescue all errors, so it's still your job to rescue errors higher in the call stack.
19
63
 
20
- Rack::Robustness is therefore a try/catch mechanism as a middleware, to be used along the Rack call stack as you would use a standard one in a more conventional call stack:
64
+ Rack::Robustness is therefore a try/catch/finally mechanism as a middleware, to be used along the Rack call stack as you would use a standard one in a more conventional call stack:
21
65
 
22
66
  ```java
23
67
  try {
24
68
  // main shield, typically in a main
25
-
69
+
26
70
  try {
27
71
  // try to achieve a goal here
28
72
  } catch (...) {
29
73
  // fallback to an alternative
74
+ } finally {
75
+ // ensure something is executed in all cases
30
76
  }
31
-
77
+
32
78
  // continue your flow
33
-
79
+
34
80
  } catch (...) {
35
81
  // something goes really wrong, inform the user as you can
36
82
  }
@@ -53,6 +99,8 @@ class Main < Sinatra::Base
53
99
  use Rack::Robustness do
54
100
  # fallback to an alternative
55
101
  # 3xx, 4xx errors maybe
102
+
103
+ # ensure something is executed in all cases
56
104
  end
57
105
 
58
106
  # try to achieve your goal through standard routes
@@ -60,7 +108,7 @@ class Main < Sinatra::Base
60
108
  end
61
109
  ```
62
110
 
63
- ## Examples
111
+ ## Additional examples
64
112
 
65
113
  ```ruby
66
114
  class App < Sinatra::Base
@@ -102,6 +150,9 @@ class App < Sinatra::Base
102
150
  # we use SecurityError for handling forbidden accesses.
103
151
  # The default status is 403 here
104
152
  g.on(SecurityError){|ex| 403 }
153
+
154
+ # ensure logging in all exceptional cases
155
+ g.ensure(true){|ex| env['rack.errors'].write(ex.message) }
105
156
  end
106
157
 
107
158
  get '/some/route/:id' do |id|
@@ -202,6 +253,27 @@ use Rack::Robustness do |g|
202
253
  end
203
254
  ```
204
255
 
256
+ ## Ensure common block in happy/exceptional/all cases
257
+
258
+ ```ruby
259
+ ##
260
+ # Ensure in all cases (no arg) or exceptional cases only (true)
261
+ #
262
+ use Rack::Robustness do |g|
263
+
264
+ # Ensure in all cases
265
+ g.ensure{|ex|
266
+ # ex might be nil here
267
+ }
268
+
269
+ # Ensure in exceptional cases only (for logging purposes for instance)
270
+ g.ensure(true){|ex|
271
+ # an exception occured, ex is never nil
272
+ env['rack.errors'].write("#{ex.message}\n")
273
+ }
274
+ end
275
+ ```
276
+
205
277
  ## Don't catch all!
206
278
 
207
279
  ```ruby
data/Rakefile CHANGED
@@ -1,6 +1,3 @@
1
- # We run tests by default
2
- task :default => :test
3
-
4
1
  #
5
2
  # Install all tasks found in tasks folder
6
3
  #
@@ -9,3 +6,4 @@ task :default => :test
9
6
  Dir["tasks/*.rake"].each do |taskfile|
10
7
  load taskfile
11
8
  end
9
+ task :default => :test
@@ -1,95 +1,210 @@
1
1
  module Rack
2
2
  class Robustness
3
3
 
4
- VERSION = "1.0.0".freeze
4
+ VERSION = "1.2.0".freeze
5
5
 
6
- NIL_HANDLER = lambda{|ex| nil }
7
-
8
- def initialize(app)
9
- @app = app
10
- @handlers = {}
11
- @status = 500
12
- @headers = {'Content-Type' => "text/plain"}
13
- @body = ["Sorry, a fatal error occured."]
14
- @catch_all = true
15
- yield self if block_given?
16
- on(Object){|ex| [@status, {}, @body]} if @catch_all
17
- @headers.freeze
18
- @body.freeze
19
- @handlers.freeze
6
+ def self.new(app, &bl)
7
+ return super(app) if bl.nil? and not(Robustness==self)
8
+ Class.new(self).install(&bl).new(app)
20
9
  end
21
10
 
22
11
  ##
23
12
  # Configuration
13
+ module DSL
24
14
 
25
- def no_catch_all
26
- @catch_all = false
27
- end
15
+ NIL_HANDLER = lambda{|ex| nil }
28
16
 
29
- def on(ex_class, &bl)
30
- @handlers[ex_class] = bl || NIL_HANDLER
31
- end
17
+ def inherited(x)
18
+ x.reset
19
+ end
32
20
 
33
- def status(s=nil, &bl)
34
- @status = s || bl
35
- end
21
+ def reset
22
+ @rescue_clauses = {}
23
+ @ensure_clauses = []
24
+ @status_clause = 500
25
+ @headers_clause = {'Content-Type' => "text/plain"}
26
+ @body_clause = ["Sorry, a fatal error occured."]
27
+ @response_builder = lambda{|ex| ::Rack::Response.new }
28
+ @catch_all = true
29
+ end
30
+ attr_reader :rescue_clauses, :ensure_clauses, :status_clause,
31
+ :headers_clause, :body_clause, :catch_all, :response_builder
32
+
33
+ def install
34
+ yield self if block_given?
35
+ on(Object){|ex|
36
+ [status_clause, {}, body_clause]
37
+ } if @catch_all
38
+ @headers_clause.freeze
39
+ @body_clause.freeze
40
+ @rescue_clauses.freeze
41
+ @ensure_clauses.freeze
42
+ self
43
+ end
36
44
 
37
- def headers(h=nil, &bl)
38
- if h.nil?
39
- @headers = bl
40
- else
41
- @headers.merge!(h)
45
+ def no_catch_all
46
+ @catch_all = false
42
47
  end
43
- end
44
48
 
45
- def content_type(ct=nil, &bl)
46
- headers('Content-Type' => ct || bl)
47
- end
49
+ def response(&bl)
50
+ @response_builder = bl
51
+ end
52
+
53
+ def rescue(ex_class, handler = nil, &bl)
54
+ @rescue_clauses[ex_class] = handler || bl || NIL_HANDLER
55
+ end
56
+ alias :on :rescue
57
+
58
+ def ensure(bypass_on_success = false, &bl)
59
+ @ensure_clauses << [bypass_on_success, bl]
60
+ end
61
+
62
+ def status(s=nil, &bl)
63
+ @status_clause = s || bl
64
+ end
65
+
66
+ def headers(h=nil, &bl)
67
+ if h.nil?
68
+ @headers_clause = bl
69
+ else
70
+ @headers_clause.merge!(h)
71
+ end
72
+ end
73
+
74
+ def content_type(ct=nil, &bl)
75
+ headers('Content-Type' => ct || bl)
76
+ end
77
+
78
+ def body(b=nil, &bl)
79
+ @body_clause = b.nil? ? bl : (String===b ? [ b ] : b)
80
+ end
81
+
82
+ end # module DSL
83
+ extend DSL
84
+
85
+ public
48
86
 
49
- def body(b=nil, &bl)
50
- @body = b.nil? ? bl : (String===b ? [ b ] : b)
87
+ def initialize(app)
88
+ @app = app
51
89
  end
52
90
 
53
91
  ##
54
92
  # Rack's call
55
93
 
56
94
  def call(env)
57
- @app.call(env)
95
+ dup.call!(env)
58
96
  rescue => ex
59
- handler = error_handler(ex.class)
60
- raise unless handler
61
- handle_response(handler, ex)
97
+ catch_all ? last_resort(ex) : raise(ex)
98
+ end
99
+
100
+ protected
101
+
102
+ def call!(env)
103
+ @env, @request = env, Rack::Request.new(env)
104
+ triple = @app.call(env)
105
+ handle_happy(triple)
106
+ rescue Exception => ex
107
+ handle_rescue(ex)
108
+ ensure
109
+ handle_ensure(ex)
62
110
  end
63
111
 
64
112
  private
65
113
 
66
- def handle_response(response, ex)
67
- case response
68
- when NilClass then handle_response([@status, {}, @body], ex)
69
- when Fixnum then handle_response([response, {}, @body], ex)
70
- when String then handle_response([@status, {}, response], ex)
71
- when Hash then handle_response([@status, response, @body], ex)
72
- when Proc then handle_response(response.call(ex), ex)
73
- else
74
- status, headers, body = response.map{|x| handle_value(x, ex) }
75
- [ status,
76
- handle_value(@headers, ex).merge(headers),
77
- body ]
114
+ attr_reader :env, :request, :response
115
+
116
+ [ :response_builder,
117
+ :rescue_clauses,
118
+ :ensure_clauses,
119
+ :status_clause,
120
+ :headers_clause,
121
+ :body_clause,
122
+ :catch_all ].each do |m|
123
+ define_method(m){|*args, &bl|
124
+ self.class.send(m, *args, &bl)
125
+ }
126
+ end
127
+
128
+ def handle_happy(triple)
129
+ s, h, b = triple
130
+ @response = Response.new(b, s, h)
131
+ @response.finish
132
+ end
133
+
134
+ def handle_rescue(ex)
135
+ begin
136
+ # build a response instance
137
+ @response = instance_exec(ex, &response_builder)
138
+
139
+ # populate it if a rescue clause can be found
140
+ if rescue_clause = find_rescue_clause(ex.class)
141
+ handle_error(ex, rescue_clause)
142
+ return @response.finish
143
+ end
144
+
145
+ # no_catch_all mode, let reraise it later
146
+ rescue Exception => ex2
147
+ return catch_all ? last_resort(ex2) : raise(ex2)
148
+ end
149
+
150
+ # we are in no_catch_all mode, reraise
151
+ raise(ex)
152
+ end
153
+
154
+ def handle_ensure(ex)
155
+ @response ||= begin
156
+ status, headers, body = last_resort(ex)
157
+ ::Rack::Response.new(body, status, headers)
78
158
  end
159
+ ensure_clauses.each{|(bypass,ensurer)|
160
+ instance_exec(ex, &ensurer) if ex or not(bypass)
161
+ }
79
162
  end
80
163
 
81
- def handle_value(value, ex)
82
- case value
83
- when Proc then value.call(ex)
84
- when Hash then value.each_with_object({}){|(k,v),h| h[k] = handle_value(v, ex)}
164
+ def handle_error(ex, rescue_clause)
165
+ case rescue_clause
166
+ when NilClass then handle_error(ex, [status_clause, {}, body_clause])
167
+ when Integer then handle_error(ex, [rescue_clause, {}, body_clause])
168
+ when String then handle_error(ex, [status_clause, {}, rescue_clause])
169
+ when Hash then handle_error(ex, [status_clause, rescue_clause, body_clause])
170
+ when Proc then handle_error(ex, handle_value(ex, rescue_clause))
85
171
  else
86
- value
172
+ status, headers, body = rescue_clause
173
+ handle_status(ex, status)
174
+ handle_headers(ex, headers)
175
+ handle_headers(ex, headers_clause)
176
+ handle_body(ex, body)
177
+ end
178
+ end
179
+
180
+ def handle_status(ex, status)
181
+ @response.status = handle_value(ex, status)
182
+ end
183
+
184
+ def handle_headers(ex, headers)
185
+ handle_value(ex, headers).each_pair do |key,value|
186
+ @response[key] ||= handle_value(ex, value)
87
187
  end
88
188
  end
89
189
 
90
- def error_handler(ex_class)
190
+ def handle_body(ex, body)
191
+ body = handle_value(ex, body)
192
+ @response.body = body.is_a?(String) ? [ body ] : body
193
+ end
194
+
195
+ def handle_value(ex, value)
196
+ value.is_a?(Proc) ? instance_exec(ex, &value) : value
197
+ end
198
+
199
+ def find_rescue_clause(ex_class)
91
200
  return nil if ex_class.nil?
92
- @handlers.fetch(ex_class){ error_handler(ex_class.superclass) }
201
+ rescue_clauses.fetch(ex_class){ find_rescue_clause(ex_class.superclass) }
202
+ end
203
+
204
+ def last_resort(ex)
205
+ [ 500,
206
+ {'Content-Type' => 'text/plain'},
207
+ [ 'An internal error occured, sorry for the disagreement.' ] ]
93
208
  end
94
209
 
95
210
  end # class Robustness
data/spec/spec_helper.rb CHANGED
@@ -4,6 +4,29 @@ require 'rack/robustness'
4
4
  require 'rack/test'
5
5
 
6
6
  module SpecHelpers
7
+
8
+ def mock_app(clazz = Rack::Robustness, &bl)
9
+ Rack::Builder.new do
10
+ use clazz, &bl
11
+ map '/happy' do
12
+ run lambda{|env| [200, {'Content-Type' => 'text/plain'}, ['happy']]}
13
+ end
14
+ map "/argument-error" do
15
+ run lambda{|env| raise ArgumentError, "an argument error" }
16
+ end
17
+ map "/type-error" do
18
+ run lambda{|env| raise TypeError, "a type error" }
19
+ end
20
+ map "/security-error" do
21
+ run lambda{|env| raise SecurityError, "a security error" }
22
+ end
23
+ end
24
+ end
25
+
26
+ def app
27
+ mock_app{}
28
+ end
29
+
7
30
  end
8
31
 
9
32
  RSpec.configure do |c|
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ describe Rack::Robustness, 'the context in which blocks execute' do
3
+ include Rack::Test::Methods
4
+
5
+ let(:app){
6
+ mock_app do |g|
7
+ g.response{|ex|
8
+ raise "Invalid context" unless env && request
9
+ Rack::Response.new
10
+ }
11
+ g.body{|ex|
12
+ raise "Invalid context" unless env && request && response
13
+ if response.status == 400
14
+ "argument-error"
15
+ else
16
+ "security-error"
17
+ end
18
+ }
19
+ g.rescue(ArgumentError){|ex|
20
+ raise "Invalid context" unless env && request && response
21
+ 400
22
+ }
23
+ g.rescue(SecurityError){|ex|
24
+ raise "Invalid context" unless env && request && response
25
+ 403
26
+ }
27
+ g.ensure{|ex|
28
+ raise "Invalid context" unless env && request && response
29
+ $seen_ex = ex
30
+ }
31
+ end
32
+ }
33
+
34
+ it 'should let `env`, `request` and `response` be available in all blocks' do
35
+ get '/argument-error'
36
+ expect(last_response.status).to eq(400)
37
+ expect(last_response.body).to eq('argument-error')
38
+ end
39
+
40
+ it 'executes the ensure block as well' do
41
+ get '/argument-error'
42
+ expect($seen_ex).to be_a(ArgumentError)
43
+ end
44
+
45
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ describe Rack::Robustness, 'ensure' do
3
+ include Rack::Test::Methods
4
+
5
+ let(:app){
6
+ mock_app do |g|
7
+ g.ensure(true) {|ex| $seen_true = [ex.class] }
8
+ g.ensure(false){|ex| $seen_false = [ex.class] }
9
+ g.ensure {|ex| $seen_none = [ex.class] }
10
+ g.status 400
11
+ g.on(ArgumentError){|ex| "error" }
12
+ end
13
+ }
14
+
15
+ before do
16
+ $seen_true = $seen_false = $seen_none = nil
17
+ end
18
+
19
+ it 'should be called in all cases when an error occurs' do
20
+ get '/argument-error'
21
+ expect(last_response.status).to eq(400)
22
+ expect(last_response.body).to eq("error")
23
+ expect($seen_true).to eq([ArgumentError])
24
+ expect($seen_false).to eq([ArgumentError])
25
+ expect($seen_none).to eq([ArgumentError])
26
+ end
27
+
28
+ it 'should not be called when explicit bypass on happy paths' do
29
+ get '/happy'
30
+ expect(last_response.status).to eq(200)
31
+ expect(last_response.body).to eq("happy")
32
+ expect($seen_true).to be_nil
33
+ expect($seen_false).to eq([NilClass])
34
+ expect($seen_none).to eq([NilClass])
35
+ end
36
+
37
+ end