├── .github └── workflows │ ├── integration.yml │ └── releases.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENCE.md ├── Makefile ├── Manifest.txt ├── README.md ├── Rakefile ├── lib └── rack │ └── robustness.rb ├── rack-robustness.gemspec ├── spec ├── spec_helper.rb ├── test_context.rb ├── test_ensure.rb ├── test_last_resort.rb ├── test_rescue.rb ├── test_response.rb ├── test_robustness.rb └── test_subclass.rb └── tasks ├── gem.rake └── test.rake /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_call: 11 | 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: ['2.7', '3.1', '3.2'] 20 | env: 21 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 22 | steps: 23 | - uses: act10ns/slack@v1 24 | with: 25 | status: starting 26 | channel: '#opensource-cicd' 27 | 28 | - uses: actions/checkout@v2 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby }} 33 | bundler-cache: true 34 | 35 | - run: make tests 36 | id: tests 37 | 38 | - uses: act10ns/slack@v1 39 | with: 40 | status: ${{ job.status }} 41 | steps: ${{ toJson(steps) }} 42 | channel: '#opensource-cicd' 43 | if: always() 44 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 10 | 11 | jobs: 12 | ci: 13 | uses: enspirit/rack-robustness/.github/workflows/integration.yml@master 14 | 15 | rubygem: 16 | needs: ci 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: act10ns/slack@v1 20 | with: 21 | status: starting 22 | channel: '#opensource-cicd' 23 | 24 | - uses: actions/checkout@v2 25 | 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: 2.7 29 | bundler-cache: true 30 | 31 | - run: make package 32 | 33 | - name: Release Gem 34 | uses: cadwallion/publish-rubygems-action@master 35 | id: gem-push 36 | env: 37 | RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} 38 | RELEASE_COMMAND: make gem.push 39 | 40 | - uses: act10ns/slack@v1 41 | with: 42 | status: ${{ job.status }} 43 | steps: ${{ toJson(steps) }} 44 | channel: '#opensource-cicd' 45 | if: always() 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | *.DS_Store 3 | pkg 4 | doc/api 5 | .yardoc 6 | .bundle -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without=release doc 3 | rvm: 4 | - 1.9.2 5 | - 1.9.3 6 | - 2.0.0 7 | - ruby-head 8 | - jruby-19mode 9 | - jruby-head 10 | - rbx-19mode 11 | - rbx-head 12 | matrix: 13 | allow_failures: 14 | - rvm: rbx-19mode 15 | - rvm: rbx-head -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 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 56 | 57 | * Enhancements 58 | 59 | * Birthday! 60 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | group :development do 4 | gem "rack", "~> 2" 5 | gem "rake", "~> 13" 6 | gem "rspec", "~> 3" 7 | gem "rack-test", "~> 0.6" 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | diff-lcs (1.5.0) 5 | rack (2.2.7) 6 | rack-test (0.6.2) 7 | rack (>= 1.0) 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) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | rack (~> 2) 28 | rack-test (~> 0.6) 29 | rake (~> 13) 30 | rspec (~> 3) 31 | 32 | BUNDLED WITH 33 | 2.4.6 34 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 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. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tests.unit: 2 | bundle exec rake test 3 | 4 | tests: tests.unit 5 | 6 | package: 7 | bundle exec rake package 8 | 9 | gem.push: 10 | ls pkg/rack-robustness-*.gem | xargs gem push 11 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | rack-robustness.gemspec 2 | rack-robustness.noespec 3 | .gemtest 4 | CHANGELOG.md 5 | Gemfile 6 | Gemfile.lock 7 | bin/**/* 8 | lib/**/* 9 | LICENCE.md 10 | Manifest.txt 11 | Rakefile 12 | README.md 13 | spec/**/* 14 | tasks/**/* 15 | test/**/* 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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. 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 | 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 | ```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 | 51 | ## Links 52 | 53 | * https://github.com/blambeau/rack-robustness 54 | * http://www.revision-zero.org/rack-robustness 55 | 56 | ## Why? 57 | 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: 59 | 60 | * 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. 61 | * The behavior to adopt when obstacles occur is not necessary defined where the exception is thrown, but often higher in the call stack. 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. 63 | 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: 65 | 66 | ```java 67 | try { 68 | // main shield, typically in a main 69 | 70 | try { 71 | // try to achieve a goal here 72 | } catch (...) { 73 | // fallback to an alternative 74 | } finally { 75 | // ensure something is executed in all cases 76 | } 77 | 78 | // continue your flow 79 | 80 | } catch (...) { 81 | // something goes really wrong, inform the user as you can 82 | } 83 | ``` 84 | 85 | becomes: 86 | 87 | ```ruby 88 | class Main < Sinatra::Base 89 | 90 | # main shield, main = rack top level 91 | use Rack::Robustness do 92 | # something goes really wrong, inform the user as you can 93 | # probably a 5xx http status here 94 | end 95 | 96 | # continue your flow 97 | use Other::Useful::Middlewares 98 | 99 | use Rack::Robustness do 100 | # fallback to an alternative 101 | # 3xx, 4xx errors maybe 102 | 103 | # ensure something is executed in all cases 104 | end 105 | 106 | # try to achieve your goal through standard routes 107 | 108 | end 109 | ``` 110 | 111 | ## Additional examples 112 | 113 | ```ruby 114 | class App < Sinatra::Base 115 | 116 | ## 117 | # Catch everything but hide root causes, for security reasons, for instance. 118 | # 119 | # This handler should never be fired unless the application has a bug... 120 | # 121 | use Rack::Robustness do |g| 122 | g.status 500 123 | g.content_type 'text/plain' 124 | g.body 'A fatal error occured.' 125 | end 126 | 127 | ## 128 | # Some middleware here for logging, content length of whatever. 129 | # 130 | # Those middleware might fail, even if unlikely. 131 | # 132 | use ... 133 | use ... 134 | 135 | ## 136 | # Catch some exceptions that denote client errors by convention in our app. 137 | # 138 | # Those exceptions are considered safe, so the message is sent to the user. 139 | # 140 | use Rack::Robustness do |g| 141 | g.no_catch_all # do not catch all errors 142 | 143 | g.status 400 # default status to 400, client error 144 | g.content_type 'text/plain' # a default content-type, maybe 145 | g.body{|ex| ex.message } # by default, send the message 146 | 147 | # catch ArgumentError, it denotes a coercion error in our app 148 | g.on(ArgumentError) 149 | 150 | # we use SecurityError for handling forbidden accesses. 151 | # The default status is 403 here 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) } 156 | end 157 | 158 | get '/some/route/:id' do |id| 159 | id = Integer(id) # will raise an ArgumentError if +id+ not an integer 160 | 161 | ... 162 | end 163 | 164 | get '/private' do |id| 165 | raise SecurityError unless logged? 166 | 167 | ... 168 | end 169 | 170 | end 171 | ``` 172 | 173 | ## Without configuration 174 | 175 | ```ruby 176 | ## 177 | # Catches all errors. 178 | # 179 | # Respond with 180 | # status: 500, 181 | # headers: {'Content-Type' => 'text/plain'} 182 | # body: [ "Sorry, an error occured." ] 183 | # 184 | use Rack::Robustness 185 | ``` 186 | 187 | ## Specifying static status, headers and/or body 188 | 189 | ```ruby 190 | ## 191 | # Catches all errors. 192 | # 193 | # Respond as specified. 194 | # 195 | use Rack::Robustness do |g| 196 | g.status 400 197 | g.headers 'Content-Type' => 'text/html' 198 | g.content_type 'text/html' # shortcut over headers 199 | g.body "

an error occured

" 200 | end 201 | ``` 202 | 203 | ## Specifying dynamic status, content_type and/or body 204 | 205 | ```ruby 206 | ## 207 | # Catches all errors. 208 | # 209 | # Respond as specified. 210 | # 211 | use Rack::Robustness do |g| 212 | g.status{|ex| ArgumentError===ex ? 400 : 500 } 213 | 214 | # global dynamic headers 215 | g.headers{|ex| {'Content-Type' => 'text/plain', ...} } 216 | 217 | # local dynamic and/or static headers 218 | g.headers 'Content-Type' => lambda{|ex| ... }, 219 | 'Foo' => 'Bar' 220 | 221 | # dynamic content type 222 | g.content_type{|ex| ...} 223 | 224 | # dynamic body (String allowed here) 225 | g.body{|ex| ex.message } 226 | end 227 | ``` 228 | 229 | ## Specific behavior for specific errors 230 | 231 | ```ruby 232 | ## 233 | # Catches all errors using defaults as above 234 | # 235 | # Respond to specific errors as specified by 'on' clauses. 236 | # 237 | use Rack::Robustness do |g| 238 | g.status 500 # this is the default behavior, as above 239 | g.content_type 'text/plain' # ... 240 | 241 | # Override status on TypeError and descendants 242 | g.on(TypeError){|ex| 400 } 243 | 244 | # Override body on ArgumentError and descendants 245 | g.on(ArgumentError){|ex| ex.message } 246 | 247 | # Override everything on SecurityError and descendants 248 | # Default headers will be merged with returned ones so content-type will be 249 | # "text/plain" unless specified below 250 | g.on(SecurityError){|ex| 251 | [ 403, { ... }, [ "Forbidden, sorry" ] ] 252 | } 253 | end 254 | ``` 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 | 277 | ## Don't catch all! 278 | 279 | ```ruby 280 | ## 281 | # Catches only errors specified in 'on' clauses, using defaults as above 282 | # 283 | # Re-raise unrecognized errors 284 | # 285 | use Rack::Robustness do |g| 286 | g.no_catch_all 287 | 288 | g.on(TypeError){|ex| 400 } 289 | ... 290 | end 291 | ``` 292 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # Install all tasks found in tasks folder 3 | # 4 | # See .rake files there for complete documentation. 5 | # 6 | Dir["tasks/*.rake"].each do |taskfile| 7 | load taskfile 8 | end 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /lib/rack/robustness.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Robustness 3 | 4 | VERSION = "1.2.0".freeze 5 | 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) 9 | end 10 | 11 | ## 12 | # Configuration 13 | module DSL 14 | 15 | NIL_HANDLER = lambda{|ex| nil } 16 | 17 | def inherited(x) 18 | x.reset 19 | end 20 | 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 44 | 45 | def no_catch_all 46 | @catch_all = false 47 | end 48 | 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 86 | 87 | def initialize(app) 88 | @app = app 89 | end 90 | 91 | ## 92 | # Rack's call 93 | 94 | def call(env) 95 | dup.call!(env) 96 | rescue => 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) 110 | end 111 | 112 | private 113 | 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) 158 | end 159 | ensure_clauses.each{|(bypass,ensurer)| 160 | instance_exec(ex, &ensurer) if ex or not(bypass) 161 | } 162 | end 163 | 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)) 171 | else 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) 187 | end 188 | end 189 | 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) 200 | return nil if ex_class.nil? 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.' ] ] 208 | end 209 | 210 | end # class Robustness 211 | end # module Rack 212 | -------------------------------------------------------------------------------- /rack-robustness.gemspec: -------------------------------------------------------------------------------- 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 189 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'rack' 3 | require 'rack/robustness' 4 | require 'rack/test' 5 | 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 | 30 | end 31 | 32 | RSpec.configure do |c| 33 | c.include SpecHelpers 34 | end 35 | -------------------------------------------------------------------------------- /spec/test_context.rb: -------------------------------------------------------------------------------- 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 46 | -------------------------------------------------------------------------------- /spec/test_ensure.rb: -------------------------------------------------------------------------------- 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 38 | -------------------------------------------------------------------------------- /spec/test_last_resort.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe Rack::Robustness, 'last resort' do 3 | include Rack::Test::Methods 4 | 5 | before do 6 | $seen_ex = nil 7 | end 8 | 9 | context 'when the response cannot be built and no catch all' do 10 | let(:app){ 11 | mock_app do |g| 12 | g.no_catch_all 13 | g.response{|ex| NoSuchResponseClass.new } 14 | g.ensure(true){|ex| $seen_ex = ex } 15 | end 16 | } 17 | 18 | it 'reraises the internal error' do 19 | lambda{ 20 | get '/argument-error' 21 | }.should raise_error(NameError, /NoSuchResponseClass/) 22 | end 23 | 24 | it 'passes into the ensure block with the original error' do 25 | lambda{ 26 | get '/argument-error' 27 | }.should raise_error(NameError, /NoSuchResponseClass/) 28 | expect($seen_ex).to be_a(ArgumentError) 29 | end 30 | end 31 | 32 | context 'when the response cannot be built and catch all' do 33 | let(:app){ 34 | mock_app do |g| 35 | g.response{|ex| NoSuchResponseClass.new } 36 | end 37 | } 38 | 39 | it 'falls back to last resort response' do 40 | get '/argument-error' 41 | expect(last_response.status).to eq(500) 42 | expect(last_response.content_type).to eq("text/plain") 43 | expect(last_response.body).to eq("An internal error occured, sorry for the disagreement.") 44 | end 45 | end 46 | 47 | context 'when an ensure block raises an error and no catch all' do 48 | let(:app){ 49 | mock_app do |g| 50 | g.no_catch_all 51 | g.ensure{|ex| NoSuchResponseClass.new } 52 | end 53 | } 54 | 55 | it 'reraises the internal error' do 56 | lambda{ 57 | get '/argument-error' 58 | }.should raise_error(NameError, /NoSuchResponseClass/) 59 | end 60 | end 61 | 62 | context 'when an ensure block raises an error and catch all' do 63 | let(:app){ 64 | mock_app do |g| 65 | g.ensure{|ex| NoSuchResponseClass.new } 66 | end 67 | } 68 | 69 | it 'reraises the internal error' do 70 | get '/argument-error' 71 | expect(last_response.status).to eq(500) 72 | expect(last_response.content_type).to eq("text/plain") 73 | expect(last_response.body).to eq("An internal error occured, sorry for the disagreement.") 74 | end 75 | end 76 | 77 | context 'when the response block fails and the ensure block uses the response object' do 78 | let(:app){ 79 | mock_app do |g| 80 | g.response{|ex| NoSuchResponseClass.new } 81 | g.ensure{|ex| $seen_response = response } 82 | end 83 | } 84 | 85 | before do 86 | $seen_response = nil 87 | end 88 | 89 | it 'sets a default response object for the ensure clause' do 90 | get '/argument-error' 91 | expect(last_response.status).to eq(500) 92 | expect(last_response.content_type).to eq("text/plain") 93 | expect(last_response.body).to eq("An internal error occured, sorry for the disagreement.") 94 | expect($seen_response).to_not be_nil 95 | end 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /spec/test_rescue.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe Rack::Robustness, 'rescue' do 3 | include Rack::Test::Methods 4 | 5 | let(:app){ 6 | mock_app do |g| 7 | g.status 400 8 | g.rescue(ArgumentError){|ex| 'argument-error' } 9 | g.rescue(SecurityError, 'security-error') 10 | g.on(TypeError) {|ex| 'type-error' } 11 | end 12 | } 13 | 14 | it 'correctly rescues specified errors' do 15 | get '/argument-error' 16 | expect(last_response.status).to eq(400) 17 | expect(last_response.body).to eq("argument-error") 18 | end 19 | 20 | it 'correctly support a non-block shortcut' do 21 | get '/security-error' 22 | expect(last_response.status).to eq(400) 23 | expect(last_response.body).to eq("security-error") 24 | end 25 | 26 | it 'is has a `on` alias' do 27 | get '/type-error' 28 | expect(last_response.status).to eq(400) 29 | expect(last_response.body).to eq("type-error") 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/test_response.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe Rack::Robustness, 'response' do 3 | include Rack::Test::Methods 4 | 5 | class MyFooResponse < Rack::Response 6 | 7 | def initialize(*args) 8 | super 9 | self['Content-Type'] = "application/json" 10 | end 11 | 12 | def each 13 | yield("response text") 14 | end 15 | 16 | end 17 | 18 | let(:app){ 19 | mock_app do |g| 20 | g.status 400 21 | g.response{|ex| MyFooResponse.new } 22 | end 23 | } 24 | 25 | it 'correctly sets the status' do 26 | get '/argument-error' 27 | expect(last_response.status).to eq(400) 28 | end 29 | 30 | xit 'correctly sets the body' do 31 | get '/argument-error' 32 | expect(last_response.body).to eq("response text") 33 | end 34 | 35 | it 'correctly sets the content type' do 36 | get '/argument-error' 37 | expect(last_response.content_type).to eq("application/json") 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/test_robustness.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe Rack::Robustness do 3 | include Rack::Test::Methods 4 | 5 | shared_examples_for 'A transparent middleware for happy paths' do 6 | 7 | it 'let happy responses unchanged' do 8 | get '/happy' 9 | expect(last_response.status).to eq(200) 10 | expect(last_response.content_type).to eq('text/plain') 11 | expect(last_response.body).to eq('happy') 12 | end 13 | end 14 | 15 | context 'with the default configuration' do 16 | let(:app){ 17 | mock_app 18 | } 19 | 20 | it_should_behave_like 'A transparent middleware for happy paths' 21 | 22 | it 'set a status 500 with a standard error message by default' do 23 | get '/argument-error' 24 | expect(last_response.status).to eq(500) 25 | expect(last_response.content_type).to eq("text/plain") 26 | expect(last_response.body).to eq("Sorry, a fatal error occured.") 27 | end 28 | 29 | it 'catches all exceptions by default' do 30 | get '/security-error' 31 | expect(last_response.status).to eq(500) 32 | expect(last_response.content_type).to eq("text/plain") 33 | expect(last_response.body).to eq("Sorry, a fatal error occured.") 34 | end 35 | end 36 | 37 | context 'with a status, content_type and body constants' do 38 | let(:app){ 39 | mock_app do |g| 40 | g.status 501 41 | g.content_type "text/test" 42 | g.body "An error occured" 43 | end 44 | } 45 | 46 | it_should_behave_like 'A transparent middleware for happy paths' 47 | 48 | it 'set the specified status and body on errors' do 49 | get '/argument-error' 50 | expect(last_response.status).to eq(501) 51 | expect(last_response.content_type).to eq("text/test") 52 | expect(last_response.body).to eq("An error occured") 53 | end 54 | end 55 | 56 | context 'with headers' do 57 | let(:app){ 58 | mock_app do |g| 59 | g.headers 'Content-Type' => 'text/test', 60 | 'Foo' => 'Bar' 61 | end 62 | } 63 | 64 | it_should_behave_like 'A transparent middleware for happy paths' 65 | 66 | it 'set the specified headers on error' do 67 | get '/argument-error' 68 | last_response.headers['Foo'].should eq('Bar') 69 | expect(last_response.content_type).to eq("text/test") 70 | end 71 | end 72 | 73 | context 'with a dynamic status, content_type and body' do 74 | let(:app){ 75 | mock_app do |g| 76 | g.status {|ex| ArgumentError===ex ? 400 : 500} 77 | g.content_type{|ex| ArgumentError===ex ? "text/arg" : 'text/other'} 78 | g.body {|ex| ex.message } 79 | end 80 | } 81 | 82 | it_should_behave_like 'A transparent middleware for happy paths' 83 | 84 | it 'correctly sets the status, content_type and body on ArgumentError' do 85 | get '/argument-error' 86 | expect(last_response.status).to eq(400) 87 | expect(last_response.content_type).to eq('text/arg') 88 | expect(last_response.body).to eq('an argument error') 89 | end 90 | 91 | it 'correctly sets the status, content_type and body on TypeError' do 92 | get '/type-error' 93 | expect(last_response.status).to eq(500) 94 | expect(last_response.content_type).to eq('text/other') 95 | expect(last_response.body).to eq('a type error') 96 | end 97 | end 98 | 99 | context 'with dynamic headers I' do 100 | let(:app){ 101 | mock_app do |g| 102 | g.headers{|ex| 103 | {'Content-Type' => ArgumentError===ex ? "text/arg" : 'text/other' } 104 | } 105 | end 106 | } 107 | 108 | it_should_behave_like 'A transparent middleware for happy paths' 109 | 110 | it 'correctly sets the specified headers on an ArgumentError' do 111 | get '/argument-error' 112 | expect(last_response.content_type).to eq("text/arg") 113 | end 114 | 115 | it 'correctly sets the specified headers on a TypeError' do 116 | get '/type-error' 117 | expect(last_response.content_type).to eq("text/other") 118 | end 119 | end 120 | 121 | context 'with dynamic headers II' do 122 | let(:app){ 123 | mock_app do |g| 124 | g.headers 'Content-Type' => lambda{|ex| ArgumentError===ex ? "text/arg" : 'text/other'}, 125 | 'Foo' => 'Bar' 126 | end 127 | } 128 | 129 | it_should_behave_like 'A transparent middleware for happy paths' 130 | 131 | it 'correctly sets the specified headers on an ArgumentError' do 132 | get '/argument-error' 133 | last_response.headers['Foo'].should eq('Bar') 134 | expect(last_response.content_type).to eq("text/arg") 135 | end 136 | 137 | it 'correctly sets the specified headers on a TypeError' do 138 | get '/type-error' 139 | last_response.headers['Foo'].should eq('Bar') 140 | expect(last_response.content_type).to eq("text/other") 141 | end 142 | end 143 | 144 | context 'when responding to specific errors with a full response' do 145 | let(:app){ 146 | mock_app do |g| 147 | g.headers 'Foo' => 'Bar', 'Content-Type' => 'default/one' 148 | g.on(ArgumentError){|ex| [401, {'Content-Type' => 'text/arg'}, [ ex.message ] ] } 149 | g.on(TypeError){|ex| [402, {}, [ ex.message ] ] } 150 | end 151 | } 152 | 153 | after do 154 | # if merges the default headers in any way 155 | last_response.headers['Foo'].should eq('Bar') 156 | end 157 | 158 | it 'uses the response on ArgumentError' do 159 | get '/argument-error' 160 | expect(last_response.status).to eq(401) 161 | expect(last_response.content_type).to eq('text/arg') 162 | expect(last_response.body).to eq("an argument error") 163 | end 164 | 165 | it 'uses the response on TypeError' do 166 | get '/type-error' 167 | expect(last_response.status).to eq(402) 168 | expect(last_response.content_type).to eq('default/one') 169 | expect(last_response.body).to eq("a type error") 170 | end 171 | end 172 | 173 | context 'when responding to specific errors with a single status' do 174 | let(:app){ 175 | mock_app do |g| 176 | g.on(ArgumentError){|ex| 401 } 177 | end 178 | } 179 | 180 | it 'uses the status and fallback to defaults for the rest' do 181 | get '/argument-error' 182 | expect(last_response.status).to eq(401) 183 | expect(last_response.content_type).to eq('text/plain') 184 | expect(last_response.body).to eq("Sorry, a fatal error occured.") 185 | end 186 | end 187 | 188 | context 'when responding to specific errors with a single body' do 189 | let(:app){ 190 | mock_app do |g| 191 | g.on(ArgumentError){|ex| ex.message } 192 | end 193 | } 194 | 195 | it 'uses it as body and fallback to defaults for the rest' do 196 | get '/argument-error' 197 | expect(last_response.status).to eq(500) 198 | expect(last_response.content_type).to eq('text/plain') 199 | expect(last_response.body).to eq("an argument error") 200 | end 201 | end 202 | 203 | context 'when configured with no_catch_all' do 204 | let(:app){ 205 | mock_app do |g| 206 | g.no_catch_all 207 | g.on(ArgumentError){|ex| 401 } 208 | end 209 | } 210 | 211 | it 'matches known errors' do 212 | get '/argument-error' 213 | expect(last_response.status).to eq(401) 214 | end 215 | 216 | it 'raises on unknown error' do 217 | lambda{ 218 | get '/type-error' 219 | }.should raise_error(TypeError) 220 | end 221 | end 222 | 223 | context 'when responding to specific errors without body' do 224 | let(:app){ 225 | mock_app do |g| 226 | g.no_catch_all 227 | g.status(401) 228 | g.on(ArgumentError) 229 | end 230 | } 231 | 232 | it 'matches known errors' do 233 | get '/argument-error' 234 | expect(last_response.status).to eq(401) 235 | expect(last_response.body).to eq("Sorry, a fatal error occured.") 236 | end 237 | 238 | it 'raises on unknown error' do 239 | lambda{ 240 | get '/type-error' 241 | }.should raise_error(TypeError) 242 | end 243 | end 244 | 245 | end 246 | -------------------------------------------------------------------------------- /spec/test_subclass.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe "Rack::Robustness subclasses" do 3 | include Rack::Test::Methods 4 | 5 | class Shield < Rack::Robustness 6 | self.body{|ex| ex.message } 7 | self.rescue(ArgumentError){|ex| 400 } 8 | end 9 | 10 | let(:app){ 11 | mock_app(Shield) 12 | } 13 | 14 | it 'works as expected' do 15 | get '/argument-error' 16 | expect(last_response.status).to eq(400) 17 | expect(last_response.body).to eq("an argument error") 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /tasks/gem.rake: -------------------------------------------------------------------------------- 1 | require 'rubygems/package_task' 2 | 3 | # Dynamically load the gem spec 4 | gemspec_file = File.expand_path('../../rack-robustness.gemspec', __FILE__) 5 | gemspec = Kernel.eval(File.read(gemspec_file)) 6 | 7 | Gem::PackageTask.new(gemspec) do |t| 8 | 9 | # Name of the package 10 | t.name = gemspec.name 11 | 12 | # Version of the package 13 | t.version = gemspec.version 14 | 15 | # Directory used to store the package files 16 | t.package_dir = "pkg" 17 | 18 | # True if a gzipped tar file (tgz) should be produced 19 | t.need_tar = false 20 | 21 | # True if a gzipped tar file (tar.gz) should be produced 22 | t.need_tar_gz = false 23 | 24 | # True if a bzip2'd tar file (tar.bz2) should be produced 25 | t.need_tar_bz2 = false 26 | 27 | # True if a zip file should be produced (default is false) 28 | t.need_zip = false 29 | 30 | # List of files to be included in the package. 31 | t.package_files = gemspec.files 32 | 33 | # Tar command for gzipped or bzip2ed archives. 34 | t.tar_command = "tar" 35 | 36 | # Zip command for zipped archives. 37 | t.zip_command = "zip" 38 | 39 | end 40 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | namespace :test do 2 | require 'rspec/core/rake_task' 3 | 4 | desc %q{Run all RSpec tests} 5 | RSpec::Core::RakeTask.new(:unit) do |t| 6 | t.rspec_opts = %w[-I. -Ilib -Ispec --pattern=spec/**/test_*.rb --color .] 7 | end 8 | 9 | task :all => :"unit" 10 | end 11 | task :test => :"test:all" 12 | --------------------------------------------------------------------------------