An open API service indexing awesome lists of open source software.

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.

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.

[![Build Status](https://secure.travis-ci.org/blambeau/rack-robustness.png)](http://travis-ci.org/blambeau/rack-robustness)
[![Dependency Status](https://gemnasium.com/blambeau/rack-robustness.png)](https://gemnasium.com/blambeau/rack-robustness)

```ruby
##
#
# 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...

```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 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:

```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::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

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

```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_all

g.on(TypeError){|ex| 400 }
...
end
```