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.

Build Status Dependency Status

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