In Ruby/Sinatra, how to halt with an ERB template and error message

Why doesn't it work − use the source!

Lets look at the Sinatra source code to see why this problem doesn't work. The main Sinatra file (lib/sinatra/base.rb) is just 2043 lines long, and pretty readable code!

All halt does is:

def halt(*response)
  response = response.first if response.length == 1
  throw :halt, response
end

And exceptions are caught with:

# Dispatch a request with error handling.
def dispatch!
  invoke do
    static! if settings.static? && (request.get? || request.head?)
    filter! :before
    route!
  end
rescue ::Exception => boom
  invoke { handle_exception!(boom) }
  [..]
end

def handle_exception!(boom)
  @env['sinatra.error'] = boom
  [..]
end

But for some reason this code is never run (as tested with basic "printf-debugging"). This is because in invoke the block is run like:

# Run the block with 'throw :halt' support and apply result to the response.
def invoke
  res = catch(:halt) { yield } 
  res = [res] if Fixnum === res or String === res
  if Array === res and Fixnum === res.first
    res = res.dup
    status(res.shift)
    body(res.pop)
    headers(*res)
  elsif res.respond_to? :each
    body res
  end
  nil # avoid double setting the same response tuple twice
end

Notice the catch(:halt) here. The if Array === res and Fixnum === res.first part is what halt sets and how the response body and status code are set.

The error 403 { .. } block is run in call!:

invoke { error_block!(response.status) } unless @env['sinatra.error']

So now we understand why this doesn't work, we can look for solutions ;-)

So can I use halt some way?

Not as far as I can see. If you look at the body of the invoke method, you'll see that the body is always set when using halt. You don't want this, since you want to override the response body.

Solution

Use a "real" exception and not the halt "pseudo-exception". Sinatra doesn't seem to come with pre-defined exceptions, but the handle_exception! does look at http_status to set the correct HTTP status:

  if boom.respond_to? :http_status
    status(boom.http_status)
  elsif settings.use_code? and boom.respond_to? :code and boom.code.between? 400, 599
    status(boom.code)
  else
    status(500)
  end

So you could use something like this:

require 'sinatra'

class PermissionDenied < StandardError
    def http_status; 403 end
end

get '/error' do
    #halt 403, 'My special message to you!'
    raise PermissionDenied, 'My special message to you!'
end

error 403 do
    'Error message -> ' +  @env['sinatra.error'].message
end

Which works as expected (the output is Error message -> My special message to you!). You can return an ERB template here.


In Sinatra v2.0.7+, messages passed to halt are stored in the body of the response. So a halt with an error code and an error message (eg: halt 403, "Message!") can be caught and rendered in an error page template with:

error 403 do
  erb :"errors/error", locals: { message: body[0] }
end

Tags:

Ruby

Erb

Sinatra