I am using Mailchimp (via the Gibbon gem) to add email addresses to my Mailchimp mailing list, and I want to handle any errors that are returned by Mailchimp and display them in my view.
Here is my Pages controller:
class PagesController < ApplicationController
def subscribe
email = subscriber_params[:email]
if email.empty?
flash[:error] = 'Please provide an email.'
redirect_to root_path
else
subscriber = Mailchimp.new.upsert(email)
if subscriber
flash[:success] = 'You\'re in!'
redirect_to root_path(subscribed: :true)
else
# Return error coming from Mailchimp (i.e. Gibbon::MailChimpError)
end
end
end
end
And here is the app > services > mailchimp.rb file I set up to separate out the Mailchimp logic:
class Mailchimp
def initialize
#gibbon = Gibbon::Request.new(api_key: Rails.application.credentials.mailchimp[:api_key])
#list_id = Rails.application.credentials.mailchimp[:list_id]
end
def upsert(email)
begin
#gibbon.lists(#list_id).members.create(
body: {
email_address: email,
status: "subscribed"
}
)
rescue Gibbon::MailChimpError => e #This is at the bottom of the Gibbon README
raise e.detail
end
end
end
What I'm trying to figure out is how to return/send Gibbon::MailChimpError back to my Pages#subscribe action. I see it being outputted as a RuntimeError in my console, but I'm not sure the right way to access/pass it along.
And please let me know if there's a better practice for this kind of implementation.
You could move the begin/rescue block to the subscribe action inside your controller to handle the error from there, or even better, you can use rescue_from in your controller like this:
class PagesController < ApplicationController
rescue_from Gibbon::MailChimpError do |e|
# Handle the exception however you want
end
def subscribe
# ....
end
end
Related
This is my controller
class Api::V1::WebhooksController < ApplicationController
include Api::V1::WebhookHelper
include Api::V1::LogHelper
skip_before_action :verify_authenticity_token
after_action :handle_wehbook
# Schedule plan
def schedule_plan
add_log(status: "webhook", message: "New Plan is scheduled.")
end
def handle_wehbook
if webhook_verified?
webhook_verified!
render status: 200, json: { error: 'webhook is verified.' }
else
webhook_verified!(verified: false)
render status: 500, json: { error: 'webhook is not verified.' }
end
end
end
This is Webhook Helper.
I am sure in WebhookHelper, it never redirects or renders anything.
require 'openssl'
require 'base64'
module Api::V1::WebhookHelper
include Api::V1::LogHelper
def webhook_verified?
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, secret, request.body.read)
hash = Base64.encode64(hmac).strip
hash == signature
end
def secret
ENV["API_KEY"]
end
def signature
request.headers["HTTP_X_SIGNATURE"]
end
def webhook_verified!(verified: true)
if verified
add_log(status: "webhook", message: "Webhook is verified.") # only puts log
else
add_log(status: "webhook", type: "warning", message: "Webhook is not verified.") # only puts log
end
end
end
I am getting this issue.
AbstractController::DoubleRenderError (Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".):
app/controllers/api/v1/webhooks_controller.rb:31:in `handle_wehbook'
I am not sure I am calling render or redirect multiple times in my action.
Anyone can help me?
Your handle_wehbook function is an after_action, it runs after some other action.
The latter has already rendered something (it may be an rails error or a redirect) thus the double render
I was handed a project that another developer worked on, without leaving any documentation behind. The code fetches some purchases from a shopping website, looks for a price and notifies the user.
The app may encounter errors like "no results found" and then I raise a standarderror.
I want to redirect the user to the error page and notify them about it but I can't do that because it isn't a controller, so the redirect_to option doesn't work.
services/purchase_checker.rb is called once an hour:
def call
user.transaction do
store_purchase
if better_purchase?
update_purchase
end
end
rescue MyError=> e
store_error(e)
end
def store_error(error)
user.check_errors.create!(error_type: error.class.name, message: error.message)
end
services/my_error.rb:
class MyError< StandardError
def initialize(error_type, error_message)
super(error_message)
#error_type = error_type
end
attr_reader :error_type
end
services/purchase_fetcher.rb:
def parse_result_page
raise purchase_form_page.error if purchase_form_page.error.present?
offer = purchase_page.map{|proposal_section|
propose(proposal_section, purchase) }
.min_by(&:price)
offer or raise MyError.new("No results", "No results could be found")
end
you should create another err class, eg NotFoundError:
offer or raise NotFoundError.new("No results", "No results could be found")
then in your controller:
begin
parse_result_page
rescue NotFoundError => e
redirect_to err_page, :notice => e.message
end
Since this is running in a job, the best way to notify the user would be by email, or some other async notification method. When an error is detected, an email is sent.
If that's not an option for some reason, you can check if a user has check_errors in any relevant controllers. Looking at the store_error(error) method that is called when an error is found, it seems it's creating a new record in the Database to log the error. You should be able to check if a user has any error logged via the user.check_errors relationship.
You could do it like this, for example:
class SomeController < ActionController::Base
# ...
before_action :redirect_if_check_errors
# ...
def redirect_if_check_errors
# Assuming you're using Devise or something similar
if current_user && current_user.check_errors.exists?
redirect_to some_error_page_you_create_for_this_path
end
end
end
This will check for these errors in every action of SomeController and redirect the user to an error page you should create, where you render the errors in the user.check_errors relationship.
There are multiple ways to do this, but I still think sending an email from the Job is a better option if you want to actively notify the user. Or perhaps add an interface element that warns the user whenever user.check_errors has stuff there, for example.
I propose that you do this synchronously so that the response can happen directly in the request/response cycle. Perhaps something like this:
# controller
def search
# do your searching
# ...
if search_results.blank?
# call model method, but do it synchrously
purchase_check = PurchaseChecker.call
end
if purchase_check.is_a?(MyError) # Check if it's your error
redirect_to(some_path, flash: { warning: "Warn them"})
end
end
# model, say PurchaseChecker
def call
# do your code
rescue MyError => e
store_error(e)
e # return the error so that the controller can do something with it
end
I am building a simple web app that sends SMS messages to cell phones using Twilio. I want to ensure that the user has entered a full 10 digit phone number before it will allow a message to attempt to be sent.
When I test it with a less-than or greater-than 10 digit number, in heroku logs, I see Twilio::REST::RequestError (The 'To' number 1234567890 is not a valid phone number.).
I have tried to use a begin/rescue wrapper and am telling it to render text: "Try again with a valid number." and tried a variety of if statements to try to avoid the error.
I am pretty new to Ruby and Rails and Twilio, but I promise i have been through every guide I have found. Any help is greatly appreciated. Full code of my UserController below:
require 'twilio-ruby'
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(params[:user])
account_sid = '...'
auth_token = '...'
if #user.save
render text: "Wasn't that fun? Hit the back button in your browser to give it another go!"
begin
client = Twilio::REST::Client.new account_sid, auth_token
client.account.sms.messages.create(
from: '+16035093259',
to: #user.phone,
body: #user.message
)
rescue Twilio::REST::RequestError
render text: "Try again with a valid number."
end
else
render :new
end
end
end
I'd extract the SMS sending logic into a separate model/controller and use a background job to process the submitting. The UserController should only handle, well, user creation/modification.
Scaffolding:
$ rails g model sms_job user:references message:text phone submitted_at:datetime
$ rake db:migrate
Model:
class SmsJob < AR::Base
attr_accessible :user_id, :message, :phone
belongs_to :user
validates_presence_of :message, :phone, :user_id
validates :phone,
length: { min: 10 },
format: { with: /\+?\d+/ }
scope :unsubmitted, where(submitted_at: nil)
TWILIO = {
from_no: '...',
account_sid: '...',
auth_token: '...'
}
# find a way to call this method separately from user request
def self.process!
unsubmitted.find_each do |job|
begin
client = Twilio::REST::Client.new TWILIO[:account_sid], TWILIO[:auth_token]
client.account.sms.messages.create(
from: TWILIO[:from_no],
to: job.phone,
body: job.message
)
job.submitted_at = Time.zone.now
job.save
rescue Twilio::REST::RequestError
# maybe set update a tries counter
# or delete job record
# or just ignore this error
end
end
end
end
The controller then should just provide the information that the SMS is going to be send:
# don't forget the 'resources :sms_jobs' in your routes.rb
class SmsJobsController < ApplicationController
# index, update, destroy only for only admin?
def new
#sms_job = SmsJobs.new
end
def create
#sms_job = current_user.sms_jobs.build params[:sms_job]
if #sms_job.save
redirect_to root_url, notice: "Your message is being send!"
else
render :new
end
end
end
For the background processing, have a look at these excellent Railscasts :-) You probably need to workaround some concurrency problems if you have to process many messages and/or Twilio has a long response time (didn't use that service yet).
I've created this code to show specific errors messages to user:
application_controller.rb
class ApplicationController < ActionController::Base
rescue_from Exception do |exception|
message = exception.message
message = "default error message" if exception.message.nil?
render :text => message
end
end
room_controller.rb
class RoomController < ApplicationController
def show
#room = Room.find(params[:room_id]) # Can throw 'ActiveRecord::RecordNotFound'
end
def business_method
# something
raise ValidationErros::BusinessException("You cant do this") if something #message "You cant do this" should be shown for user
#...
end
def business_method_2
Room.find(params[:room_id]).do_something
end
end
room.rb
class Room < ActiveRecord::Base
def do_something
#...
raise ValidationErrors::BusinessException("Invalid state for room") if something #message "Invalid state for room" should be shown for user
#...
end
end
app/models/erros/validation_errors.rb
module ValidationErrors
class BusinessException < RuntimeError
attr :message
def initialize(message = nil)
#message = message
end
end
end
example.js
$.ajax({
url: '/room/show/' + roomId,
success: function(data){
//... do something with data
},
error: function(data){
App.notifyError(data) //show dialog with message
}
});
But I can not use the class BusinessException. When BusinessException should be raised,
the message
uninitialized constant Room::ValidationErrors
is shown to user.
if I change this code:
raise ValidationErrors::BusinessException("Invalid state for room") if something
by this:
raise "Invalid state for room" if something
It works.
What change to this code works with BusinessException with messages. I need this
to create specifics rescue_from methods in ApplicationController.
Edit:
Thank you for comments!
My error is it doesn't know ValidationErrors Module. How to import this Module to my class?
I've tested add theses lines to lines:
require 'app/models/errors/validation_errors.rb'
require 'app/models/errors/validation_errors'
But then raise the error:
cannot load such file -- app/models/errors/validation_errors
Solution:
https://stackoverflow.com/a/3356843/740394
config.autoload_paths += %W(#{config.root}/app/models/errors)
raise ::ValidationErrors::BusinessException("Invalid state for room")
I have a create method that calls a method in a model that pings some third-party APIs.
What I need to do is if the API sends back a certain message, then I'd display an error.
Below is my current controller and model setup, so how would I get the error back in to the controller (and ultimately the view)?
Here is the method in my controller:
def create
#number = Number.where(:tracking => params[:number][:tracking], :user_id => current_user.id).first
if #number.blank?
#number = Number.new
#number.tracking = params[:number][:tracking]
#number.user_id = current_user.id
#number.notes = params[:number][:notes]
#number.track
end
respond_with(#number) do |format|
format.html { redirect_to root_path }
end
end
Here are the methods in my model:
def track
create_events
end
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
self.assign_tracker(tracker)
tracker.events.each do |e|
self.create_event(e) unless (block_given? && !block.call(e))
end
save
else
# NEED TO THROW THE ERROR HERE
end
end
How about if rather than throwing errors, you just use validation? Something like the following (Just to get your started. This would need work.):
# if you don't cache the tracker in an attribute already, do this so
# you can add errors as if it were a column.
attr_accessor :tracker
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
# ...
else
# add the error with i18n
errors.add(:tracker, :error_type_if_you_know_it)
# or add it from a returned message
errors.add(:tracker, nil, :message => fedex.get_error())
end
end
Then in your controller:
#number.track
respond_with(#number) do |format|
if #number.errors.any?
format.html { redirect_to root_path }
else
format.html { render :some_template_with_errors }
end
end
Alternatively you could do this as part of validation (so calling valid? would work as expected and not destroy your custom "track" errors)
# do your tracking on creation, if number was given
validate :on => :create do
if number.present?
tracker = fedex.track(:tracking_number => number)
unless tracker.valid?
errors.add :tracker, nil, :message => tracker.get_error()
end
end
end
# then do your actual creation of tracking events sometime after validation
before_save :handle_tracker_assignment
def handle_tracker_assignment
self.assign_tracker(tracker)
# note the block method you're using would need to be reworked
# ...
end
Note in the latter case you'd have to change your logic a bit, and simply pass the tracking number and attempt to save a new record, which would trigger the tracking attempt.
You should typically offload the the API calls to a background job and you could either use notifiers (or Rack middleware) to raise self-defined errors and handle them accordingly.