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.
##
#
# The middleware you would have written
#
class Robustness
def initialize(app)
@app = app
end
def 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
end
end
...becomes...
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
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:
try {
// main shield, typically in a main
try {
// 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:
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::Middlewares
use 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
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 errors
g.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) }
end
get '/some/route/:id' do |id|
id = Integer(id) # will raise an ArgumentError if +id+ not an integer
...
end
get '/private' do |id|
raise SecurityError unless logged?
...
end
end
Without configuration
##
# 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
##
# 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 "<p>an error occured</p>"
end
Specifying dynamic status, content_type and/or body
##
# 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
##
# 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
##
# 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!
##
# Catches only errors specified in 'on' clauses, using defaults as above
#
# Re-raise unrecognized errors
#
use Rack::Robustness do |g|
g.no_catch_all
g.on(TypeError){|ex| 400 }
...
end