Override Sinatra default NotFound error page - ruby

Is there a way to override the sinatra default NotFound error page ("Sinatra doesnt know this ditty")? I want sinatra to show only a plain string as "Method not found" when it does not found the proper route, but when I raise an 404 error from inside a route I want it to show the passed-in error message.
Implementing the not_found block like this:
not_found do
'Method not found.'
end
works, but its not a valid option since I want to be able to throw my own NotFound error messages from routes like this:
get '/' do
begin
# some processing that can raise an exception if resource not found
rescue => e
error 404, e.message.to_json
end
end
But as expected not_found block overrides my error message.

Perhaps a more graceful solution than that proposed in the accepted answer is to rescue only Sinatra::NotFound, rather than using the error(404) or not_found styles.
error Sinatra::NotFound do
content_type 'text/plain'
[404, 'Not Found']
end
This prevents the "sinatra doesn't know this ditty" default page for routes that you haven't defined, but doesn't get in the way of explicit return [404, 'Something else']-style responses.

If you don't use error handling in your route, you can utilize the built in error route like this (taken and modified from the Sinatra: Up and Running book)
require 'sinatra'
configure do
set :show_exceptions, false
end
get '/div_by_zero' do
0 / 0
"You won't see me."
end
not_found do
request.path
end
error do
"Error is: " + params['captures'].first.inspect
end
There is a parameter captures that holds your error. You can access it via params['captures']. It is an array, and in my tests it would contain a single element, which was the error itself (not a string).
Here is information on the request object.

Nevermind, found that all routes are matched in order, so after all routes I put get/post/put/delete '*' do ; end and that solves my problem.

Related

how to send SystemExit message to server

So I have a Sinatra API containing this piece of code in a model:
def self.delete(account_id)
# using Sequel, not ActiveRecord:
if Account[account_id][:default] == true
abort("Impossible to delete a default account. Please first set another account to default.")
end
# rest of the code
end
then, in app.rb :
delete '/account/:id' do
if Account.delete(params[:id]) == 1
status 200
else
status 500
end
end
On the client side (vuejs app), I would like the error message to be displayed. Instead, when the error produces, I get a SystemExit with the error message.
How do I send that SystemExit message to the server?
In Ruby in general you want to break out of execution either by using return, exceptions or throw/catch. abort is rarely if ever used as it will immediately halt execution of the entire program - and prevent you from doing stuff like cleaning up or actually sending a meaningful response to the client besides whatever error page the web server will render if you just quit the job half way though.
You can easily implement this by creating your own exception class:
class AccountDeletionError < StandardError
end
def self.delete(account_id)
# using Sequel, not ActiveRecord:
if Account[account_id][:default] == true
raise AccountDeletionError.new, "Impossible to delete a default account. Please first set another account to default."
end
end
delete '/account/:id' do
if Account.delete(params[:id]) == 1
status 200
end
rescue AccountDeletionError => e
status 409 # not 500.
content_type :json
{ error: e.message }.to_json
end
end
Now that you know how to handle errors you should probally address the next possible one - when an account cannot be found.

Raising 500 errors deliberately in Sinatra in order to test how they are handled

I want to write an RSpec test which verifies that, should a 500 error occur in my Sinatra-powered API, the error will be caught by a Sinatra error definition and returned to the client in a JSON format. That is, rather than returning some HTML error page, it returns JSON like this to conform with the rest of the API:
{
success: "false",
response: "Internal server error"
}
However, I'm unsure how to actually trigger a 500 error in my Sinatra app in order to test this behaviour with RSpec. I can't find a way to mock Sinatra routes, so currently my best idea is this route which deliberately causes a 500. This feels like a pretty dreadful solution:
get '/api/v1/testing/internal-server-error' do
1 / 0
end
Is there a way to mock Sinatra routes so that I can have, say, /'s route handler block raise an exception, therefore triggering a 500? If not, is there some other way to deliberately cause a 500 error in my app?
When facing a situation like this, what I usually do is separate concerns, and move logic outside of the Sinatra get ... block. Then, it is easy to stub it and make it raise an error.
For example, given this server code:
# server.rb
require 'sinatra'
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
You can then use this code to have the model, or any other class/function you are using to actually generate the response, raise an error, using this spec:
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
For completeness, here is a self contained file that can be tested to demonstrate this approach by running rspec thisfile.rb:
# thisfile.rb
require 'rack/test'
require 'rspec'
require 'sinatra'
# server
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
# spec_helper
ENV['APP_ENV'] = 'test'
module RSpecMixin
include Rack::Test::Methods
def app() Sinatra::Application end
end
RSpec.configure do |c|
c.include RSpecMixin
end
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
Use the halt method:
require 'sinatra'
get '/' do
halt 500, {
success: 'false',
response: 'Internal server error'
}.to_json
end

Sinatra Error Handling in Ruby

I have a simple Sinatra rest and I am having trouble trapping an error. I also admit I am fairly new to Ruby and Sinatra.
When I raise and error in the post endpoint I want to report the incoming document. I need to either 1) handle the error within the post result (where I have access to #incoming) or 2) pass the incoming document to the error and report it there.
What is a better option, option 1 or option 2?
If I stay with option 1, how do I prevent error from picking up the error (as it seems to be doing now)
If I go to option 2, how do I pass incoming to error?
Below is a sample of my code:
post ('/result') do
begin
#incoming = JSON.parse(request.body.read)
//do something that causes an error
rescue
e = env['sinatra.error']
url = request.url
ip = request.ip
#actlogpassblock = { :message => e.message,
:path => url,
:ip => ip,
:timestamp => Time.new,
:type => "500",
:sub => "RES",
:payload => #incoming
}
action_log.insert(#actlogpassblock)
status 500
end
end
error do
status 500
e = env['sinatra.error']
url = request.url
ip = request.ip
backtrace = "Application error\n#{e}\n#{e.backtrace.join("\n")}"
#actlogpassblock = { :message => e.message,
:path => url,
:ip => ip,
:timestamp => Time.new,
:type => "500",
:backtrace => backtrace
}
action_log.insert(#actlogpassblock)
{:result => 'Ah Shucks! Something went wrong'}.to_json
end
If I stay with option 1, how do I prevent error from picking up the
error (as it seems to be doing now)
According to the docs:
The error handler is invoked any time an exception is raised from a
route block...
However, that only applies to uncaught exceptions. Try this:
require 'sinatra'
set :show_exceptions, false
get '/' do
begin
raise ZeroDivisionError
rescue ZeroDivisionError
"rescue clause"
end
end
error do
"sinatra error handler"
end
Then try this:
get '/' do
raise ZeroDivisionError
end
error do
"sinatra error handler"
end
Also, you can tailor the error handler to only catch certain exceptions thereby avoiding some exceptions, e.g.
error IndexError do ...
or
error MyCustomException do ...
or
error 400..510 do
But for the catch all version:
error do
you can't stop that from executing when an uncaught exception occurs in a route block...unless:
The error handlers will only be invoked, however, if both the Sinatra
:raise_errors and :show_exceptions configuration options have been set
to false...
:raise_errors defaults to true in the "test" environment and to false
on other environments.
:show_exceptions value defaults to true in the
"development" environment and to false on other environments
The author of Sintra has said: "This [behavior] is
intentional. The idea is that error blocks will hide the issue and you
usually don't want to do this in development mode.
https://github.com/sul-dlss/sdr-services-app/blob/master/Sinatra-error-handling.md
If I go to option 2, how do I pass incoming to error?
An #variable that is created inside a route block can be seen inside an error block. Try this:
require 'sinatra'
set :show_exceptions, false
get '/' do
#incoming = "hello world" #create #variable
raise ZeroDivisionError
end
error ZeroDivisionError do
#incoming #retrieve #variable
end
After entering the url http://localhost:4567 in your browser, you should see "hello world" on the returned web page.
The reason that works is because an #variable attaches itself to whatever object is self at the time the #variable is created; likewise when an #variable is summoned, it is retrieved from whatever object is self at that time. When Sinatra executes either the route block or the error block it sets self to the same object.
Option 2 seems nice because it separates the error handling code from the application code.

Unhandled routes in espresso

Unhandled routes in espresso don't seem to raise a 404, instead I see this in the browser "max params accepted: 0; params given: 1". Is there a way to universally have it routed to an error handler instead?
Thanks!
That's more about params rather than routes.
"X params accepted, Y given" message are returned by a resolved action.
most likely you have something like:
def some_action
# ...
end
and calling it like /some_action/something
when :some_action responds only to /some_action/
To handle these errors simply add a 404 error handler:
class App < E
error 404 do |error|
# render your styled error page
end
# actions
end

How to raise a custom error code in sinatra?

I did the following in my sinatra app:
disable :show_exceptions
disable :raise_errors
error do
haml :error, :locals => {:error_message => request.env['sinatra.error'].to_s}
end
get '/error' do
raise "ERROR!!"
end
If I visit /error I get a 500 - Internal Server Error response code, which is god and wanted. But how do I change the code to, eg, 404 or 501?
The answer:
disable :show_exceptions
disable :raise_errors
get '/error' do
halt(404,haml(:error, :locals => {:error_message => request.env['sinatra.error'].to_s}))
end
Something like raise 404 raises an error just like raise ZeroDivisionError would, which causes your app to throw a 500 Internal Server Error. The simplest way to return a specific error is to use status
get '/raise404' do
status 404
end
You can also add a custom response body with body
get '/raise403' do
status 403
body 'This is a 403 error'
end
I use this in block
if 'condition' then
do something
else
halt 500 , "error message"
end
#only without error
erb :my_template
In case of error my log is like this
HTTP/1.1" 500 13 0.1000
Instead of raise "ERROR!!", try just doing error 404 or error 501 with optional status message after the status code.
Update:
If you define your error handler as
error 400..501 do... for example, you can use error 501 "ERROR!!" in your "/error" route. This will also put your "ERROR!!" message in env['sinatra.error'].message.

Resources