0.03
Low commit activity in last 3 years
A long-lived project that still receives updates
Rack::Robustness provides you with an easy way to handle errors in your stack, for making web applications more robust.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.5
~> 0.6
~> 10.0
~> 2.12
 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.

Build Status Dependency Status

##
#
# 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