https://github.com/enspirit/rack-robustness
Rack::Robustness, the rescue clause of your Rack stack.
https://github.com/enspirit/rack-robustness
Last synced: 14 days ago
JSON representation
Rack::Robustness, the rescue clause of your Rack stack.
- Host: GitHub
- URL: https://github.com/enspirit/rack-robustness
- Owner: enspirit
- License: mit
- Created: 2013-02-14T17:30:27.000Z (about 12 years ago)
- Default Branch: master
- Last Pushed: 2023-06-09T08:11:17.000Z (almost 2 years ago)
- Last Synced: 2025-04-04T09:37:50.136Z (22 days ago)
- Language: Ruby
- Homepage:
- Size: 36.1 KB
- Stars: 82
- Watchers: 6
- Forks: 5
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Rack::Robustness, the rescue clause of your Rack stack.
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.
[](http://travis-ci.org/blambeau/rack-robustness)
[](https://gemnasium.com/blambeau/rack-robustness)```ruby
##
#
# The middleware you would have written
#
class Robustnessdef initialize(app)
@app = app
enddef call(env)
@app.call(env)
rescue ArgumentError => ex
[400, { 'Content-Type' => 'text/plain' }, [ ex.message ] ] # suppose the message can be safely used
rescue SecurityError => ex
[403, { 'Content-Type' => 'text/plain' }, [ ex.message ] ]
ensure
env['rack.errors'].write(ex.message) if ex
endend
```...becomes...
```ruby
use Rack::Robustness do |g|
g.on(ArgumentError){|ex| 400 }
g.on(SecurityError){|ex| 403 }g.content_type 'text/plain'
g.body{|ex|
ex.message
}g.ensure(true){|ex|
env['rack.errors'].write(ex.message)
}
end
```## Links
* https://github.com/blambeau/rack-robustness
* http://www.revision-zero.org/rack-robustness## Why?
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:
* 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.
* The behavior to adopt when obstacles occur is not necessary defined where the exception is thrown, but often higher in the call stack.
* 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.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:
```java
try {
// main shield, typically in a maintry {
// try to achieve a goal here
} catch (...) {
// fallback to an alternative
} finally {
// ensure something is executed in all cases
}// continue your flow
} catch (...) {
// something goes really wrong, inform the user as you can
}
```becomes:
```ruby
class Main < Sinatra::Base# main shield, main = rack top level
use Rack::Robustness do
# something goes really wrong, inform the user as you can
# probably a 5xx http status here
end# continue your flow
use Other::Useful::Middlewaresuse Rack::Robustness do
# fallback to an alternative
# 3xx, 4xx errors maybe# ensure something is executed in all cases
end# try to achieve your goal through standard routes
end
```## Additional examples
```ruby
class App < Sinatra::Base##
# Catch everything but hide root causes, for security reasons, for instance.
#
# This handler should never be fired unless the application has a bug...
#
use Rack::Robustness do |g|
g.status 500
g.content_type 'text/plain'
g.body 'A fatal error occured.'
end##
# Some middleware here for logging, content length of whatever.
#
# Those middleware might fail, even if unlikely.
#
use ...
use ...##
# Catch some exceptions that denote client errors by convention in our app.
#
# Those exceptions are considered safe, so the message is sent to the user.
#
use Rack::Robustness do |g|
g.no_catch_all # do not catch all errorsg.status 400 # default status to 400, client error
g.content_type 'text/plain' # a default content-type, maybe
g.body{|ex| ex.message } # by default, send the message# catch ArgumentError, it denotes a coercion error in our app
g.on(ArgumentError)# we use SecurityError for handling forbidden accesses.
# The default status is 403 here
g.on(SecurityError){|ex| 403 }# ensure logging in all exceptional cases
g.ensure(true){|ex| env['rack.errors'].write(ex.message) }
endget '/some/route/:id' do |id|
id = Integer(id) # will raise an ArgumentError if +id+ not an integer...
endget '/private' do |id|
raise SecurityError unless logged?...
endend
```## Without configuration
```ruby
##
# Catches all errors.
#
# Respond with
# status: 500,
# headers: {'Content-Type' => 'text/plain'}
# body: [ "Sorry, an error occured." ]
#
use Rack::Robustness
```## Specifying static status, headers and/or body
```ruby
##
# Catches all errors.
#
# Respond as specified.
#
use Rack::Robustness do |g|
g.status 400
g.headers 'Content-Type' => 'text/html'
g.content_type 'text/html' # shortcut over headers
g.body "an error occured
"
end
```## Specifying dynamic status, content_type and/or body
```ruby
##
# Catches all errors.
#
# Respond as specified.
#
use Rack::Robustness do |g|
g.status{|ex| ArgumentError===ex ? 400 : 500 }# global dynamic headers
g.headers{|ex| {'Content-Type' => 'text/plain', ...} }# local dynamic and/or static headers
g.headers 'Content-Type' => lambda{|ex| ... },
'Foo' => 'Bar'# dynamic content type
g.content_type{|ex| ...}# dynamic body (String allowed here)
g.body{|ex| ex.message }
end
```## Specific behavior for specific errors
```ruby
##
# Catches all errors using defaults as above
#
# Respond to specific errors as specified by 'on' clauses.
#
use Rack::Robustness do |g|
g.status 500 # this is the default behavior, as above
g.content_type 'text/plain' # ...# Override status on TypeError and descendants
g.on(TypeError){|ex| 400 }# Override body on ArgumentError and descendants
g.on(ArgumentError){|ex| ex.message }# Override everything on SecurityError and descendants
# Default headers will be merged with returned ones so content-type will be
# "text/plain" unless specified below
g.on(SecurityError){|ex|
[ 403, { ... }, [ "Forbidden, sorry" ] ]
}
end
```## Ensure common block in happy/exceptional/all cases
```ruby
##
# Ensure in all cases (no arg) or exceptional cases only (true)
#
use Rack::Robustness do |g|# Ensure in all cases
g.ensure{|ex|
# ex might be nil here
}# Ensure in exceptional cases only (for logging purposes for instance)
g.ensure(true){|ex|
# an exception occured, ex is never nil
env['rack.errors'].write("#{ex.message}\n")
}
end
```## Don't catch all!
```ruby
##
# Catches only errors specified in 'on' clauses, using defaults as above
#
# Re-raise unrecognized errors
#
use Rack::Robustness do |g|
g.no_catch_allg.on(TypeError){|ex| 400 }
...
end
```