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. It scales from zero configuration (a default shield) to specific rescue clauses for specific errors.
Links
https://github.com/blambeau/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 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
}
// 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
end
# try to achieve your goal through standard routes
end
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 }
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
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