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.)
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 ;-)
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With