how to send SystemExit message to server - ruby

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.

Related

Sidekiq transient vs fatal errors

Is there a way to err from a Sidekiq job in a way that tells Sidekiq that "this error is fatal and unrecoverable, do not retry, send it straight to dead job queue"?
Looking at Sidekiq Error Handling documentation, it seems like it interpret all errors as transient, and will retry a job (if retry is enabled) regardless of the error type.
You should rescue those specific errors and not re-raise them.
def perform
call_something
rescue CustomException
nil
end
Edit:
Well, if you want to purposely send a message to the DLQ/DJQ, you'd need to make a method that does what #send_to_morgue does. I'm sure Mike Perham is going to come in here and yell at me for suggesting this but...
def send_to_morgue(msg)
Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
payload = Sidekiq.dump_json(msg)
now = Time.now.to_f
Sidekiq.redis do |conn|
conn.multi do
conn.zadd('dead', now, payload)
conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
end
end
end
The only difference you'd have to dig into what msg looks like going into that method but I suspect it's what normally hits the middleware before parse.
If found on GitHub a solution for your problem. In that post they suggested to write a custom middleware that handles the exceptions you want to prevent retries for. This is a basic example:
def call(worker, msg, queue)
begin
yield
rescue ActiveRecord::RecordNotFound => e
msg['retry'] = false
raise
end
end
You can extending that you get:
def call(worker, msg, queue)
begin
yield
rescue ActiveRecord::RecordNotFound => e
msg['retry'] = false
raise
rescue Exception => e
if worker.respond_to?(:handle_error)
worker.handle_error(e)
else
raise
end
end
end

How should I return Sinatra HTTP errors from inside a class where HALT is not available?

I have a large backend API for my native app that's built in Sinatra, that also serves some admin web pages. I'm trying to dry up the codebase and refactor code into classes inside the lib directory.
My API clients expect a status and a message, such as 200 OK, or 404 Profile Not Found. I'd usually do this with something like halt 404, 'Profile Not Found'.
What's the easiest way of halting with an HTTP status code and a message from inside a class?
Old Wet Code
post '/api/process_something'
halt 403, 'missing profile_id' unless params[:profile_id].present?
halt 404, 'offer not found' unless params[:offer_id].present?
do_some_processing
200
end
New Dry Code
post '/api/process_something'
offer_manager = OfferManager.new
offer_manager.process_offer(params: params)
end
offer_manager.rb
class OfferManager
def process_offer(params:)
# halt 403, 'missing profile_id' unless params[:profile_id].present?
# halt 404, 'offer not found' unless params[:offer_id].present?
# halt doesn't work from in here
do_some_processing
200
end
end
This question is probably better for CodeReview but one approach you can see in an OO design here is a 'halt' path and a 'happy' path. Your class just needs to implement a few methods to help this be consistent across all your sinatra routes and methods.
Here's one approach, and it would be easy to adopt this kind of interface across other classes using inheritance.
post '/api/process_something' do
offer_manager = OfferManager.new(params)
# error guard clause
halt offer_manager.status, offer_manager.halt_message if offer_manager.halt?
# validations met, continue to process
offer_manager.process_offer
# return back 200
offer_manager.status
end
class OfferManager
attr_reader :status, :params, :halt_message
def initialize(params)
#params = params
validate_params
end
def process_offer
do_some_processing
end
def halt?
# right now we just know missing params is one error to halt on but this is where
# you could implement more business logic if need be
missing_params?
end
private
def validate_params
if missing_params?
#status = 404
#halt_message = "missing #{missing_keys.join(", ")} key(s)"
else
#status = 200
end
end
def do_some_processing
# go do other processing
end
def missing_params?
missing_keys.size > 0
end
def missing_keys
expected_keys = [:profile_id, :offer_id]
params.select { |k, _| !expected_keys.has_key?(k) }
end
end

rescue_from Koala exceptions

Beginner question perhaps:
I'm trying to check my user permissions from facebook with Koala. In some cases I'm going to get thrown an error. So I just want to catch it and redirect to re-authenticate.
def check_facebook_permissions
if token = current_user.try(:authentications).find_by_provider('facebook').try(:token)
graph = Koala::Facebook::API.new(token)
permissions = graph.get_connections('me','permissions')
session[:facebook] = {}
session[:facebook][:ask_publish_actions] = true if permissions[0]['publish_actions'] != true && permissions[0]['publish_stream'] != true
end
rescue_from Koala::Facebook::APIError
# Do something funky here
end
I thought this was straightforward, but I'm never hitting my rescue. Instead I get:
Koala::Facebook::APIError (OAuthException: Error validating access token: Session has expired at unix time 1324026000. The current unix time is 1324352685.):
What am I missing here?
rescue_from is not a syntactic construct of Ruby like rescue is - it is a normal function, and you need a block to go with it. In your code, no code is given, rescue_from gets executed and effectively skipped - what is after it has no bearing on any exceptions raised before it (just as if you put any other function, like puts, instead of rescue_from).
See an example of rescue_from use here.
To make this code work, you need the vanilla Ruby rescue:
rescue Koala::Facebook::APIError => e
The correct syntax for handling errors in Ruby is:
begin
# do something that will throw an error
rescue StandardError => e # StandardError is the root class of most errors
# rescue the error
end

Override Sinatra default NotFound error page

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.

Accessing Sinatra scope from another class

I'm running a Sinatra application with a few extra classes pulled in for creating a User and a few others on the fly (no DB, it feeds in from a web service). I'm trying send out a flash notice (using https://github.com/nakajima/rack-flash) from within my User model but can't figure out how to get access to the flash method/variable because I'm out of scope.
Something like:
class User
def something
if true
flash[:notice] = 'Good job'
else
# nope
end
end
end
Which gets required into the Sinatra app by a simple require 'models/user'
This is an XY Problem[1]. Sinatra is responsible for sending out flash messages, not your User objects, so the code for setting the flash should be in your Sinatra app, not in your User class.
[1] http://www.perlmonks.org/index.pl?node_id=542341
You should not ask your User (model) to talk to the UI (view). That's bad/not MVC-clean. That's what a controller is for.
You can use either return values, exceptions, or throw/catch (which is not exception handling) to pass information from your model to your controller. For example, using return values:
post "/create_user" do
flash[:notice] = case User.something
when User then "User Created!"
when :nono then "That's not allowed"
when :later then "User queued to be created later."
end
end
class User
def self.something
if authorized
if can_create_now
new(...)
else
queue_create(...)
:later
end
else
:nono
end
end
end
Since I mentioned them above, following are examples using throw/catch and begin/rescue (exceptions). As the advisability of using either of these constructs is questionable, let us take a moment of silence to ponder if this is a good idea.
Here is an example using throw/catch:
post "/create_user" do
result = catch(:msg){ User.something }
flash[:notice] = case
when :nono then "That's not allowed"
when :later then "User queued to be created later."
else "User Created!"
end
end
class User
def self.something
throw :msg, :nono unless authorized
if can_create_now
new(...)
else
queue_create(...)
throw :msg, :later
end
end
end
Finally, here's an example using exceptions, though I'm not convinced that this will be appropriate for all (non-disastrous) cases where you want to flash unique messages to the user:
post "/create_user" do
flash[:notice] = "User Created!" # Assume all good
begin
User.something
rescue User::Trouble=>e
flash[:notice] = case e
when Unauthorized then "That's not allowed"
when DelayedCreate then "User queued to be created later."
else "Uh...Something bad happened."
end
end
end
class User
class Trouble < RuntimeError; end
class Unauthorized < Trouble; end
class DelayedCreate < Trouble; end
def self.something
raise Unauthorized unless authorized
if can_create_now
new(...)
else
queue_create(...)
raise DelayedCreate
end
end
end
Exceptions let you pass an additional data along (e.g. raise Unauthorized.new "No such account", or any custom properties you want to add to your class), and so may be more useful (when appropriate). Just remember to pass semantic results from your model to your controller, and let it translate them into user-facing messages.

Resources