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