Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

ruby

erb

sinatra

In my Sinatra project, I'd like to be able to halt with both an error code and an error message:

halt 403, "Message!"

I want this, in turn, to be rendered in an error page template (using ERB). For example:

error 403 do
    erb :"errors/error", :locals => {:message => env['sinatra.error'].message}
end

However, apparently env['sinatra.error'].message (aka the readme and every single website says I should do it) does not expose the message I've provided. (This code, when run, returns the undefined method `message' for nil:NilClass error.)

I've searched for 4-5 hours and experimented with everything and I can't figure out where the message is exposed for me to render via ERB! Does anyone know where it is?


(It seems like the only alternative I can think of is writing this instead of the halt code above, every time I would like to halt:

halt 403, erb(:"errors/error", :locals => {m: "Message!"})

This code works. But this is a messy solution since it involves hardcoding the location of the error ERB file.)

(If you were wondering, this problem is not related to the show_exceptions configuration flag because both set :show_exceptions, false and set :show_exceptions, :after_handler make no difference.)

like image 324
Brandon Wang Avatar asked Mar 21 '16 09:03

Brandon Wang


2 Answers

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.

like image 137
Martin Tournoij Avatar answered Oct 21 '22 11:10

Martin Tournoij


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
like image 26
UrsaDK Avatar answered Oct 21 '22 09:10

UrsaDK