rack-robustness 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +25 -0
- data/LICENCE.md +22 -0
- data/Manifest.txt +15 -0
- data/README.md +219 -0
- data/Rakefile +11 -0
- data/lib/rack/robustness.rb +96 -0
- data/rack-robustness.gemspec +188 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/test_robustness.rb +253 -0
- data/tasks/gem.rake +73 -0
- data/tasks/spec_test.rake +71 -0
- metadata +131 -0
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
rack (1.5.2)
|
6
|
+
rack-test (0.6.2)
|
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)
|
17
|
+
|
18
|
+
PLATFORMS
|
19
|
+
ruby
|
20
|
+
|
21
|
+
DEPENDENCIES
|
22
|
+
rack (~> 1.5)
|
23
|
+
rack-test (~> 0.6)
|
24
|
+
rake (~> 10.0)
|
25
|
+
rspec (~> 2.12)
|
data/LICENCE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# The MIT Licence
|
2
|
+
|
3
|
+
Copyright (c) 2013 - Bernard Lambeau
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
# Rack::Robustness, the rescue clause of your Rack stack.
|
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.
|
4
|
+
|
5
|
+
[![Build Status](https://secure.travis-ci.org/blambeau/rack-robustness.png)](http://travis-ci.org/blambeau/rack-robustness)
|
6
|
+
[![Dependency Status](https://gemnasium.com/blambeau/rack-robustness.png)](https://gemnasium.com/blambeau/rack-robustness)
|
7
|
+
|
8
|
+
## Links
|
9
|
+
|
10
|
+
https://github.com/blambeau/rack-robustness
|
11
|
+
|
12
|
+
## Why?
|
13
|
+
|
14
|
+
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
|
+
|
16
|
+
* Exceptions occur, because you can't always test/control boundary conditions. E.g. your code can pro-actively test that a file exists before reading it, but it cannot pro-actively test that the user removes the network cable in the middle of a download.
|
17
|
+
* The behavior to adopt when obstacles occur is not necessary defined where the exception is thrown, but often higher in the call stack.
|
18
|
+
* 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
|
+
|
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:
|
21
|
+
|
22
|
+
```java
|
23
|
+
try {
|
24
|
+
// main shield, typically in a main
|
25
|
+
|
26
|
+
try {
|
27
|
+
// try to achieve a goal here
|
28
|
+
} catch (...) {
|
29
|
+
// fallback to an alternative
|
30
|
+
}
|
31
|
+
|
32
|
+
// continue your flow
|
33
|
+
|
34
|
+
} catch (...) {
|
35
|
+
// something goes really wrong, inform the user as you can
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
becomes:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class Main < Sinatra::Base
|
43
|
+
|
44
|
+
# main shield, main = rack top level
|
45
|
+
use Rack::Robustness do
|
46
|
+
# something goes really wrong, inform the user as you can
|
47
|
+
# probably a 5xx http status here
|
48
|
+
end
|
49
|
+
|
50
|
+
# continue your flow
|
51
|
+
use Other::Useful::Middlewares
|
52
|
+
|
53
|
+
use Rack::Robustness do
|
54
|
+
# fallback to an alternative
|
55
|
+
# 3xx, 4xx errors maybe
|
56
|
+
end
|
57
|
+
|
58
|
+
# try to achieve your goal through standard routes
|
59
|
+
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Examples
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class App < Sinatra::Base
|
67
|
+
|
68
|
+
##
|
69
|
+
# Catch everything but hide root causes, for security reasons, for instance.
|
70
|
+
#
|
71
|
+
# This handler should never be fired unless the application has a bug...
|
72
|
+
#
|
73
|
+
use Rack::Robustness do |g|
|
74
|
+
g.status 500
|
75
|
+
g.content_type 'text/plain'
|
76
|
+
g.body 'A fatal error occured.'
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Some middleware here for logging, content length of whatever.
|
81
|
+
#
|
82
|
+
# Those middleware might fail, even if unlikely.
|
83
|
+
#
|
84
|
+
use ...
|
85
|
+
use ...
|
86
|
+
|
87
|
+
##
|
88
|
+
# Catch some exceptions that denote client errors by convention in our app.
|
89
|
+
#
|
90
|
+
# Those exceptions are considered safe, so the message is sent to the user.
|
91
|
+
#
|
92
|
+
use Rack::Robustness do |g|
|
93
|
+
g.no_catch_all # do not catch all errors
|
94
|
+
|
95
|
+
g.status 400 # default status to 400, client error
|
96
|
+
g.content_type 'text/plain' # a default content-type, maybe
|
97
|
+
g.body{|ex| ex.message } # by default, send the message
|
98
|
+
|
99
|
+
# catch ArgumentError, it denotes a coercion error in our app
|
100
|
+
g.on(ArgumentError)
|
101
|
+
|
102
|
+
# we use SecurityError for handling forbidden accesses.
|
103
|
+
# The default status is 403 here
|
104
|
+
g.on(SecurityError){|ex| 403 }
|
105
|
+
end
|
106
|
+
|
107
|
+
get '/some/route/:id' do |id|
|
108
|
+
id = Integer(id) # will raise an ArgumentError if +id+ not an integer
|
109
|
+
|
110
|
+
...
|
111
|
+
end
|
112
|
+
|
113
|
+
get '/private' do |id|
|
114
|
+
raise SecurityError unless logged?
|
115
|
+
|
116
|
+
...
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
## Without configuration
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
##
|
126
|
+
# Catches all errors.
|
127
|
+
#
|
128
|
+
# Respond with
|
129
|
+
# status: 500,
|
130
|
+
# headers: {'Content-Type' => 'text/plain'}
|
131
|
+
# body: [ "Sorry, an error occured." ]
|
132
|
+
#
|
133
|
+
use Rack::Robustness
|
134
|
+
```
|
135
|
+
|
136
|
+
## Specifying static status, headers and/or body
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
##
|
140
|
+
# Catches all errors.
|
141
|
+
#
|
142
|
+
# Respond as specified.
|
143
|
+
#
|
144
|
+
use Rack::Robustness do |g|
|
145
|
+
g.status 400
|
146
|
+
g.headers 'Content-Type' => 'text/html'
|
147
|
+
g.content_type 'text/html' # shortcut over headers
|
148
|
+
g.body "<p>an error occured</p>"
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
## Specifying dynamic status, content_type and/or body
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
##
|
156
|
+
# Catches all errors.
|
157
|
+
#
|
158
|
+
# Respond as specified.
|
159
|
+
#
|
160
|
+
use Rack::Robustness do |g|
|
161
|
+
g.status{|ex| ArgumentError===ex ? 400 : 500 }
|
162
|
+
|
163
|
+
# global dynamic headers
|
164
|
+
g.headers{|ex| {'Content-Type' => 'text/plain', ...} }
|
165
|
+
|
166
|
+
# local dynamic and/or static headers
|
167
|
+
g.headers 'Content-Type' => lambda{|ex| ... },
|
168
|
+
'Foo' => 'Bar'
|
169
|
+
|
170
|
+
# dynamic content type
|
171
|
+
g.content_type{|ex| ...}
|
172
|
+
|
173
|
+
# dynamic body (String allowed here)
|
174
|
+
g.body{|ex| ex.message }
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
## Specific behavior for specific errors
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
##
|
182
|
+
# Catches all errors using defaults as above
|
183
|
+
#
|
184
|
+
# Respond to specific errors as specified by 'on' clauses.
|
185
|
+
#
|
186
|
+
use Rack::Robustness do |g|
|
187
|
+
g.status 500 # this is the default behavior, as above
|
188
|
+
g.content_type 'text/plain' # ...
|
189
|
+
|
190
|
+
# Override status on TypeError and descendants
|
191
|
+
g.on(TypeError){|ex| 400 }
|
192
|
+
|
193
|
+
# Override body on ArgumentError and descendants
|
194
|
+
g.on(ArgumentError){|ex| ex.message }
|
195
|
+
|
196
|
+
# Override everything on SecurityError and descendants
|
197
|
+
# Default headers will be merged with returned ones so content-type will be
|
198
|
+
# "text/plain" unless specified below
|
199
|
+
g.on(SecurityError){|ex|
|
200
|
+
[ 403, { ... }, [ "Forbidden, sorry" ] ]
|
201
|
+
}
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
## Don't catch all!
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
##
|
209
|
+
# Catches only errors specified in 'on' clauses, using defaults as above
|
210
|
+
#
|
211
|
+
# Re-raise unrecognized errors
|
212
|
+
#
|
213
|
+
use Rack::Robustness do |g|
|
214
|
+
g.no_catch_all
|
215
|
+
|
216
|
+
g.on(TypeError){|ex| 400 }
|
217
|
+
...
|
218
|
+
end
|
219
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module Rack
|
2
|
+
class Robustness
|
3
|
+
|
4
|
+
VERSION = "1.0.0".freeze
|
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
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Configuration
|
24
|
+
|
25
|
+
def no_catch_all
|
26
|
+
@catch_all = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def on(ex_class, &bl)
|
30
|
+
@handlers[ex_class] = bl || NIL_HANDLER
|
31
|
+
end
|
32
|
+
|
33
|
+
def status(s=nil, &bl)
|
34
|
+
@status = s || bl
|
35
|
+
end
|
36
|
+
|
37
|
+
def headers(h=nil, &bl)
|
38
|
+
if h.nil?
|
39
|
+
@headers = bl
|
40
|
+
else
|
41
|
+
@headers.merge!(h)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def content_type(ct=nil, &bl)
|
46
|
+
headers('Content-Type' => ct || bl)
|
47
|
+
end
|
48
|
+
|
49
|
+
def body(b=nil, &bl)
|
50
|
+
@body = b.nil? ? bl : (String===b ? [ b ] : b)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Rack's call
|
55
|
+
|
56
|
+
def call(env)
|
57
|
+
@app.call(env)
|
58
|
+
rescue => ex
|
59
|
+
handler = error_handler(ex.class)
|
60
|
+
raise unless handler
|
61
|
+
handle_response(handler, ex)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
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 ]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
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)}
|
85
|
+
else
|
86
|
+
value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def error_handler(ex_class)
|
91
|
+
return nil if ex_class.nil?
|
92
|
+
@handlers.fetch(ex_class){ error_handler(ex_class.superclass) }
|
93
|
+
end
|
94
|
+
|
95
|
+
end # class Robustness
|
96
|
+
end # module Rack
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# We require your library, mainly to have access to the VERSION number.
|
2
|
+
# Feel free to set $version manually.
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
4
|
+
require "rack/robustness"
|
5
|
+
$version = Rack::Robustness::VERSION.dup.to_s
|
6
|
+
|
7
|
+
#
|
8
|
+
# This is your Gem specification. Default values are provided so that your library
|
9
|
+
# should be correctly packaged given what you have described in the .noespec file.
|
10
|
+
#
|
11
|
+
Gem::Specification.new do |s|
|
12
|
+
|
13
|
+
################################################################### ABOUT YOUR GEM
|
14
|
+
|
15
|
+
# Gem name (required)
|
16
|
+
s.name = "rack-robustness"
|
17
|
+
|
18
|
+
# Gem version (required)
|
19
|
+
s.version = $version
|
20
|
+
|
21
|
+
# A short summary of this gem
|
22
|
+
#
|
23
|
+
# This is displayed in `gem list -d`.
|
24
|
+
s.summary = "Rack::Robustness, the rescue clause of your Rack stack."
|
25
|
+
|
26
|
+
# A long description of this gem (required)
|
27
|
+
#
|
28
|
+
# The description should be more detailed than the summary. For example,
|
29
|
+
# you might wish to copy the entire README into the description.
|
30
|
+
s.description = "Rack::Robustness provides you with an easy way to handle errors in your stack, for making web applications more robust."
|
31
|
+
|
32
|
+
# The URL of this gem home page (optional)
|
33
|
+
s.homepage = "https://github.com/blambeau/rack-robustness"
|
34
|
+
|
35
|
+
# Gem publication date (required but auto)
|
36
|
+
#
|
37
|
+
# Today is automatically used by default, uncomment only if
|
38
|
+
# you know what you do!
|
39
|
+
#
|
40
|
+
# s.date = Time.now.strftime('%Y-%m-%d')
|
41
|
+
|
42
|
+
# The license(s) for the library. Each license must be a short name, no
|
43
|
+
# more than 64 characters.
|
44
|
+
#
|
45
|
+
# s.licences = %w{}
|
46
|
+
|
47
|
+
# The rubyforge project this gem lives under (optional)
|
48
|
+
#
|
49
|
+
# s.rubyforge_project = nil
|
50
|
+
|
51
|
+
################################################################### ABOUT THE AUTHORS
|
52
|
+
|
53
|
+
# The list of author names who wrote this gem.
|
54
|
+
#
|
55
|
+
# If you are providing multiple authors and multiple emails they should be
|
56
|
+
# in the same order.
|
57
|
+
#
|
58
|
+
s.authors = ["Bernard Lambeau"]
|
59
|
+
|
60
|
+
# Contact emails for this gem
|
61
|
+
#
|
62
|
+
# If you are providing multiple authors and multiple emails they should be
|
63
|
+
# in the same order.
|
64
|
+
#
|
65
|
+
# NOTE: Somewhat strangly this attribute is always singular!
|
66
|
+
# Don't replace by s.emails = ...
|
67
|
+
s.email = ["blambeau@gmail.com"]
|
68
|
+
|
69
|
+
################################################################### PATHS, FILES, BINARIES
|
70
|
+
|
71
|
+
# Paths in the gem to add to $LOAD_PATH when this gem is
|
72
|
+
# activated (required).
|
73
|
+
#
|
74
|
+
# The default 'lib' is typically sufficient.
|
75
|
+
s.require_paths = ["lib"]
|
76
|
+
|
77
|
+
# Files included in this gem.
|
78
|
+
#
|
79
|
+
# By default, we take all files included in the Manifest.txt file on root
|
80
|
+
# of the project. Entries of the manifest are interpreted as Dir[...]
|
81
|
+
# patterns so that lazy people may use wilcards like lib/**/*
|
82
|
+
#
|
83
|
+
here = File.expand_path(File.dirname(__FILE__))
|
84
|
+
s.files = File.readlines(File.join(here, 'Manifest.txt')).
|
85
|
+
inject([]){|files, pattern| files + Dir[File.join(here, pattern.strip)]}.
|
86
|
+
collect{|x| x[(1+here.size)..-1]}
|
87
|
+
|
88
|
+
# Test files included in this gem.
|
89
|
+
#
|
90
|
+
s.test_files = Dir["test/**/*"] + Dir["spec/**/*"]
|
91
|
+
|
92
|
+
# The path in the gem for executable scripts (optional)
|
93
|
+
#
|
94
|
+
s.bindir = "bin"
|
95
|
+
|
96
|
+
# Executables included in the gem.
|
97
|
+
#
|
98
|
+
s.executables = (Dir["bin/*"]).collect{|f| File.basename(f)}
|
99
|
+
|
100
|
+
################################################################### REQUIREMENTS & INSTALL
|
101
|
+
# Remember the gem version requirements operators and schemes:
|
102
|
+
# = Equals version
|
103
|
+
# != Not equal to version
|
104
|
+
# > Greater than version
|
105
|
+
# < Less than version
|
106
|
+
# >= Greater than or equal to
|
107
|
+
# <= Less than or equal to
|
108
|
+
# ~> Approximately greater than
|
109
|
+
#
|
110
|
+
# Don't forget to have a look at http://lmgtfy.com/?q=Ruby+Versioning+Policies
|
111
|
+
# for setting your gem version.
|
112
|
+
#
|
113
|
+
# For your requirements to other gems, remember that
|
114
|
+
# ">= 2.2.0" (optimistic: specify minimal version)
|
115
|
+
# ">= 2.2.0", "< 3.0" (pessimistic: not greater than the next major)
|
116
|
+
# "~> 2.2" (shortcut for ">= 2.2.0", "< 3.0")
|
117
|
+
# "~> 2.2.0" (shortcut for ">= 2.2.0", "< 2.3.0")
|
118
|
+
#
|
119
|
+
|
120
|
+
#
|
121
|
+
# One call to add_dependency('gem_name', 'gem version requirement') for each
|
122
|
+
# runtime dependency. These gems will be installed with your gem.
|
123
|
+
# One call to add_development_dependency('gem_name', 'gem version requirement')
|
124
|
+
# for each development dependency. These gems are required for developers
|
125
|
+
#
|
126
|
+
s.add_development_dependency("rake", "~> 10.0")
|
127
|
+
s.add_development_dependency("rspec", "~> 2.12")
|
128
|
+
s.add_development_dependency("rack", "~> 1.5")
|
129
|
+
s.add_development_dependency("rack-test", "~> 0.6")
|
130
|
+
|
131
|
+
|
132
|
+
# The version of ruby required by this gem
|
133
|
+
#
|
134
|
+
# Uncomment and set this if your gem requires specific ruby versions.
|
135
|
+
#
|
136
|
+
# s.required_ruby_version = ">= 0"
|
137
|
+
|
138
|
+
# The RubyGems version required by this gem
|
139
|
+
#
|
140
|
+
# s.required_rubygems_version = ">= 0"
|
141
|
+
|
142
|
+
# The platform this gem runs on. See Gem::Platform for details.
|
143
|
+
#
|
144
|
+
# s.platform = nil
|
145
|
+
|
146
|
+
# Extensions to build when installing the gem.
|
147
|
+
#
|
148
|
+
# Valid types of extensions are extconf.rb files, configure scripts
|
149
|
+
# and rakefiles or mkrf_conf files.
|
150
|
+
#
|
151
|
+
s.extensions = []
|
152
|
+
|
153
|
+
# External (to RubyGems) requirements that must be met for this gem to work.
|
154
|
+
# It’s simply information for the user.
|
155
|
+
#
|
156
|
+
s.requirements = nil
|
157
|
+
|
158
|
+
# A message that gets displayed after the gem is installed
|
159
|
+
#
|
160
|
+
# Uncomment and set this if you want to say something to the user
|
161
|
+
# after gem installation
|
162
|
+
#
|
163
|
+
s.post_install_message = nil
|
164
|
+
|
165
|
+
################################################################### SECURITY
|
166
|
+
|
167
|
+
# The key used to sign this gem. See Gem::Security for details.
|
168
|
+
#
|
169
|
+
# s.signing_key = nil
|
170
|
+
|
171
|
+
# The certificate chain used to sign this gem. See Gem::Security for
|
172
|
+
# details.
|
173
|
+
#
|
174
|
+
# s.cert_chain = []
|
175
|
+
|
176
|
+
################################################################### RDOC
|
177
|
+
|
178
|
+
# An ARGV style array of options to RDoc
|
179
|
+
#
|
180
|
+
# See 'rdoc --help' about this
|
181
|
+
#
|
182
|
+
s.rdoc_options = []
|
183
|
+
|
184
|
+
# Extra files to add to RDoc such as README
|
185
|
+
#
|
186
|
+
s.extra_rdoc_files = Dir["README.md"] + Dir["CHANGELOG.md"] + Dir["LICENCE.md"]
|
187
|
+
|
188
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe Rack::Robustness do
|
3
|
+
include Rack::Test::Methods
|
4
|
+
|
5
|
+
def mock_app(&bl)
|
6
|
+
Rack::Builder.new do
|
7
|
+
use Rack::Robustness, &bl
|
8
|
+
map '/happy' do
|
9
|
+
run lambda{|env| [200, {'Content-Type' => 'text/plain'}, ['happy']]}
|
10
|
+
end
|
11
|
+
map "/argument-error" do
|
12
|
+
run lambda{|env| raise ArgumentError, "an argument error" }
|
13
|
+
end
|
14
|
+
map "/type-error" do
|
15
|
+
run lambda{|env| raise TypeError, "a type error" }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
shared_examples_for 'A transparent middleware for happy paths' do
|
21
|
+
|
22
|
+
it 'let happy responses unchanged' do
|
23
|
+
get '/happy'
|
24
|
+
last_response.status.should eq(200)
|
25
|
+
last_response.content_type.should eq('text/plain')
|
26
|
+
last_response.body.should eq('happy')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with the default configuration' do
|
31
|
+
let(:app){
|
32
|
+
mock_app
|
33
|
+
}
|
34
|
+
|
35
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
36
|
+
|
37
|
+
it 'set a status 500 with a standard error message by default' do
|
38
|
+
get '/argument-error'
|
39
|
+
last_response.status.should eq(500)
|
40
|
+
last_response.content_type.should eq("text/plain")
|
41
|
+
last_response.body.should eq("Sorry, a fatal error occured.")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'with a status, content_type and body constants' do
|
46
|
+
let(:app){
|
47
|
+
mock_app do |g|
|
48
|
+
g.status 501
|
49
|
+
g.content_type "text/test"
|
50
|
+
g.body "An error occured"
|
51
|
+
end
|
52
|
+
}
|
53
|
+
|
54
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
55
|
+
|
56
|
+
it 'set the specified status and body on errors' do
|
57
|
+
get '/argument-error'
|
58
|
+
last_response.status.should eq(501)
|
59
|
+
last_response.content_type.should eq("text/test")
|
60
|
+
last_response.body.should eq("An error occured")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with headers' do
|
65
|
+
let(:app){
|
66
|
+
mock_app do |g|
|
67
|
+
g.headers 'Content-Type' => 'text/test',
|
68
|
+
'Foo' => 'Bar'
|
69
|
+
end
|
70
|
+
}
|
71
|
+
|
72
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
73
|
+
|
74
|
+
it 'set the specified headers on error' do
|
75
|
+
get '/argument-error'
|
76
|
+
last_response.headers['Foo'].should eq('Bar')
|
77
|
+
last_response.content_type.should eq("text/test")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'with a dynamic status, content_type and body' do
|
82
|
+
let(:app){
|
83
|
+
mock_app do |g|
|
84
|
+
g.status {|ex| ArgumentError===ex ? 400 : 500}
|
85
|
+
g.content_type{|ex| ArgumentError===ex ? "text/arg" : 'text/other'}
|
86
|
+
g.body {|ex| ex.message }
|
87
|
+
end
|
88
|
+
}
|
89
|
+
|
90
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
91
|
+
|
92
|
+
it 'correctly sets the status, content_type and body on ArgumentError' do
|
93
|
+
get '/argument-error'
|
94
|
+
last_response.status.should eq(400)
|
95
|
+
last_response.content_type.should eq('text/arg')
|
96
|
+
last_response.body.should eq('an argument error')
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'correctly sets the status, content_type and body on TypeError' do
|
100
|
+
get '/type-error'
|
101
|
+
last_response.status.should eq(500)
|
102
|
+
last_response.content_type.should eq('text/other')
|
103
|
+
last_response.body.should eq('a type error')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'with dynamic headers I' do
|
108
|
+
let(:app){
|
109
|
+
mock_app do |g|
|
110
|
+
g.headers{|ex|
|
111
|
+
{'Content-Type' => ArgumentError===ex ? "text/arg" : 'text/other' }
|
112
|
+
}
|
113
|
+
end
|
114
|
+
}
|
115
|
+
|
116
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
117
|
+
|
118
|
+
it 'correctly sets the specified headers on an ArgumentError' do
|
119
|
+
get '/argument-error'
|
120
|
+
last_response.content_type.should eq("text/arg")
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'correctly sets the specified headers on a TypeError' do
|
124
|
+
get '/type-error'
|
125
|
+
last_response.content_type.should eq("text/other")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'with dynamic headers II' do
|
130
|
+
let(:app){
|
131
|
+
mock_app do |g|
|
132
|
+
g.headers 'Content-Type' => lambda{|ex| ArgumentError===ex ? "text/arg" : 'text/other'},
|
133
|
+
'Foo' => 'Bar'
|
134
|
+
end
|
135
|
+
}
|
136
|
+
|
137
|
+
it_should_behave_like 'A transparent middleware for happy paths'
|
138
|
+
|
139
|
+
it 'correctly sets the specified headers on an ArgumentError' do
|
140
|
+
get '/argument-error'
|
141
|
+
last_response.headers['Foo'].should eq('Bar')
|
142
|
+
last_response.content_type.should eq("text/arg")
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'correctly sets the specified headers on a TypeError' do
|
146
|
+
get '/type-error'
|
147
|
+
last_response.headers['Foo'].should eq('Bar')
|
148
|
+
last_response.content_type.should eq("text/other")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when responding to specific errors with a full response' do
|
153
|
+
let(:app){
|
154
|
+
mock_app do |g|
|
155
|
+
g.headers 'Foo' => 'Bar', 'Content-Type' => 'default/one'
|
156
|
+
g.on(ArgumentError){|ex| [401, {'Content-Type' => 'text/arg'}, [ ex.message ] ] }
|
157
|
+
g.on(TypeError){|ex| [402, {}, [ ex.message ] ] }
|
158
|
+
end
|
159
|
+
}
|
160
|
+
|
161
|
+
after do
|
162
|
+
# if merges the default headers in any way
|
163
|
+
last_response.headers['Foo'].should eq('Bar')
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'uses the response on ArgumentError' do
|
167
|
+
get '/argument-error'
|
168
|
+
last_response.status.should eq(401)
|
169
|
+
last_response.content_type.should eq('text/arg')
|
170
|
+
last_response.body.should eq("an argument error")
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'uses the response on TypeError' do
|
174
|
+
get '/type-error'
|
175
|
+
last_response.status.should eq(402)
|
176
|
+
last_response.content_type.should eq('default/one')
|
177
|
+
last_response.body.should eq("a type error")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when responding to specific errors with a single status' do
|
182
|
+
let(:app){
|
183
|
+
mock_app do |g|
|
184
|
+
g.on(ArgumentError){|ex| 401 }
|
185
|
+
end
|
186
|
+
}
|
187
|
+
|
188
|
+
it 'uses the status and fallback to defaults for the rest' do
|
189
|
+
get '/argument-error'
|
190
|
+
last_response.status.should eq(401)
|
191
|
+
last_response.content_type.should eq('text/plain')
|
192
|
+
last_response.body.should eq("Sorry, a fatal error occured.")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'when responding to specific errors with a single body' do
|
197
|
+
let(:app){
|
198
|
+
mock_app do |g|
|
199
|
+
g.on(ArgumentError){|ex| ex.message }
|
200
|
+
end
|
201
|
+
}
|
202
|
+
|
203
|
+
it 'uses it as body and fallback to defaults for the rest' do
|
204
|
+
get '/argument-error'
|
205
|
+
last_response.status.should eq(500)
|
206
|
+
last_response.content_type.should eq('text/plain')
|
207
|
+
last_response.body.should eq("an argument error")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
context 'when configured with no_catch_all' do
|
212
|
+
let(:app){
|
213
|
+
mock_app do |g|
|
214
|
+
g.no_catch_all
|
215
|
+
g.on(ArgumentError){|ex| 401 }
|
216
|
+
end
|
217
|
+
}
|
218
|
+
|
219
|
+
it 'matches known errors' do
|
220
|
+
get '/argument-error'
|
221
|
+
last_response.status.should eq(401)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'raises on unknown error' do
|
225
|
+
lambda{
|
226
|
+
get '/type-error'
|
227
|
+
}.should raise_error(TypeError)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'when responding to specific errors without body' do
|
232
|
+
let(:app){
|
233
|
+
mock_app do |g|
|
234
|
+
g.no_catch_all
|
235
|
+
g.status(401)
|
236
|
+
g.on(ArgumentError)
|
237
|
+
end
|
238
|
+
}
|
239
|
+
|
240
|
+
it 'matches known errors' do
|
241
|
+
get '/argument-error'
|
242
|
+
last_response.status.should eq(401)
|
243
|
+
last_response.body.should eq("Sorry, a fatal error occured.")
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'raises on unknown error' do
|
247
|
+
lambda{
|
248
|
+
get '/type-error'
|
249
|
+
}.should raise_error(TypeError)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
data/tasks/gem.rake
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Installs rake tasks for gemming and packaging
|
2
|
+
#
|
3
|
+
# This file installs the 'rake package', 'rake gem' tasks and associates
|
4
|
+
# (clobber_package, repackage, ...). It is automatically generated by Noe
|
5
|
+
# from your .noespec file, and should therefore be configured there, under
|
6
|
+
# the variables/rake_tasks/gem entry, as illustrated below:
|
7
|
+
#
|
8
|
+
# variables:
|
9
|
+
# rake_tasks:
|
10
|
+
# gem:
|
11
|
+
# package_dir: pkg
|
12
|
+
# need_tar: false
|
13
|
+
# need_tar_gz: false
|
14
|
+
# need_tar_bz2: false
|
15
|
+
# need_zip: false
|
16
|
+
# ...
|
17
|
+
#
|
18
|
+
# If you have specific needs requiring manual intervention on this file,
|
19
|
+
# don't forget to set safe-override to false in your noe specification:
|
20
|
+
#
|
21
|
+
# template-info:
|
22
|
+
# manifest:
|
23
|
+
# tasks/gem.rake:
|
24
|
+
# safe-override: false
|
25
|
+
#
|
26
|
+
begin
|
27
|
+
require 'rubygems/package_task'
|
28
|
+
|
29
|
+
# Dynamically load the gem spec
|
30
|
+
gemspec_file = File.expand_path('../../rack-robustness.gemspec', __FILE__)
|
31
|
+
gemspec = Kernel.eval(File.read(gemspec_file))
|
32
|
+
|
33
|
+
Gem::PackageTask.new(gemspec) do |t|
|
34
|
+
|
35
|
+
# Name of the package
|
36
|
+
t.name = gemspec.name
|
37
|
+
|
38
|
+
# Version of the package
|
39
|
+
t.version = gemspec.version
|
40
|
+
|
41
|
+
# Directory used to store the package files
|
42
|
+
t.package_dir = "pkg"
|
43
|
+
|
44
|
+
# True if a gzipped tar file (tgz) should be produced
|
45
|
+
t.need_tar = false
|
46
|
+
|
47
|
+
# True if a gzipped tar file (tar.gz) should be produced
|
48
|
+
t.need_tar_gz = false
|
49
|
+
|
50
|
+
# True if a bzip2'd tar file (tar.bz2) should be produced
|
51
|
+
t.need_tar_bz2 = false
|
52
|
+
|
53
|
+
# True if a zip file should be produced (default is false)
|
54
|
+
t.need_zip = false
|
55
|
+
|
56
|
+
# List of files to be included in the package.
|
57
|
+
t.package_files = gemspec.files
|
58
|
+
|
59
|
+
# Tar command for gzipped or bzip2ed archives.
|
60
|
+
t.tar_command = "tar"
|
61
|
+
|
62
|
+
# Zip command for zipped archives.
|
63
|
+
t.zip_command = "zip"
|
64
|
+
|
65
|
+
end
|
66
|
+
rescue LoadError
|
67
|
+
task :gem do
|
68
|
+
abort 'rubygems/package_task is not available. You should verify your rubygems installation'
|
69
|
+
end
|
70
|
+
task :package do
|
71
|
+
abort 'rubygems/package_task is not available. You should verify your rubygems installation'
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Installs a rake task for for running examples written using rspec.
|
2
|
+
#
|
3
|
+
# This file installs the 'rake spec_test' (aliased as 'rake spec') as well as
|
4
|
+
# extends 'rake test' to run spec tests, if any. It is automatically generated
|
5
|
+
# by Noe from your .noespec file, and should therefore be configured there,
|
6
|
+
# under the variables/rake_tasks/spec_test entry, as illustrated below:
|
7
|
+
#
|
8
|
+
# variables:
|
9
|
+
# rake_tasks:
|
10
|
+
# spec_test:
|
11
|
+
# pattern: spec/**/*_spec.rb
|
12
|
+
# verbose: true
|
13
|
+
# rspec_opts: [--color, --backtrace]
|
14
|
+
# ...
|
15
|
+
#
|
16
|
+
# If you have specific needs requiring manual intervention on this file,
|
17
|
+
# don't forget to set safe-override to false in your noe specification:
|
18
|
+
#
|
19
|
+
# template-info:
|
20
|
+
# manifest:
|
21
|
+
# tasks/spec_test.rake:
|
22
|
+
# safe-override: false
|
23
|
+
#
|
24
|
+
# This file has been written to conform to RSpec v2.4.0. More information about
|
25
|
+
# rspec and options of the rake task defined below can be found on
|
26
|
+
# http://relishapp.com/rspec
|
27
|
+
#
|
28
|
+
begin
|
29
|
+
require "rspec/core/rake_task"
|
30
|
+
desc "Run RSpec code examples"
|
31
|
+
RSpec::Core::RakeTask.new(:spec_test) do |t|
|
32
|
+
# Glob pattern to match files.
|
33
|
+
t.pattern = "spec/**/test_*.rb"
|
34
|
+
|
35
|
+
# Whether or not to fail Rake when an error occurs (typically when
|
36
|
+
# examples fail).
|
37
|
+
t.fail_on_error = true
|
38
|
+
|
39
|
+
# A message to print to stderr when there are failures.
|
40
|
+
t.failure_message = nil
|
41
|
+
|
42
|
+
# Use verbose output. If this is set to true, the task will print the
|
43
|
+
# executed spec command to stdout.
|
44
|
+
t.verbose = true
|
45
|
+
|
46
|
+
# Use rcov for code coverage?
|
47
|
+
t.rcov = false
|
48
|
+
|
49
|
+
# Path to rcov.
|
50
|
+
t.rcov_path = "rcov"
|
51
|
+
|
52
|
+
# Command line options to pass to rcov. See 'rcov --help' about this
|
53
|
+
t.rcov_opts = []
|
54
|
+
|
55
|
+
# Command line options to pass to ruby. See 'ruby --help' about this
|
56
|
+
t.ruby_opts = []
|
57
|
+
|
58
|
+
# Path to rspec
|
59
|
+
t.rspec_path = "rspec"
|
60
|
+
|
61
|
+
# Command line options to pass to rspec. See 'rspec --help' about this
|
62
|
+
t.rspec_opts = ["--color", "--backtrace"]
|
63
|
+
end
|
64
|
+
rescue LoadError => ex
|
65
|
+
task :spec_test do
|
66
|
+
abort 'rspec is not available. In order to run spec, you must: gem install rspec'
|
67
|
+
end
|
68
|
+
ensure
|
69
|
+
task :spec => [:spec_test]
|
70
|
+
task :test => [:spec_test]
|
71
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-robustness
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bernard Lambeau
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '10.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '10.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.12'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.12'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rack
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.5'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.5'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-test
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0.6'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.6'
|
78
|
+
description: Rack::Robustness provides you with an easy way to handle errors in your
|
79
|
+
stack, for making web applications more robust.
|
80
|
+
email:
|
81
|
+
- blambeau@gmail.com
|
82
|
+
executables: []
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files:
|
85
|
+
- README.md
|
86
|
+
- CHANGELOG.md
|
87
|
+
- LICENCE.md
|
88
|
+
files:
|
89
|
+
- rack-robustness.gemspec
|
90
|
+
- CHANGELOG.md
|
91
|
+
- Gemfile
|
92
|
+
- Gemfile.lock
|
93
|
+
- lib/rack/robustness.rb
|
94
|
+
- LICENCE.md
|
95
|
+
- Manifest.txt
|
96
|
+
- Rakefile
|
97
|
+
- README.md
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
- spec/test_robustness.rb
|
100
|
+
- tasks/gem.rake
|
101
|
+
- tasks/spec_test.rake
|
102
|
+
homepage: https://github.com/blambeau/rack-robustness
|
103
|
+
licenses: []
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
hash: 2934394792950840478
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.8.24
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Rack::Robustness, the rescue clause of your Rack stack.
|
129
|
+
test_files:
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
- spec/test_robustness.rb
|