I'm connecting our Rails (3.2) application to an external web service (campaign monitor) which requires a few calls to their system to set a client up.
I've put each action into separate rescue blocks and wrapped in a transaction but things aren't looking so great. I need it to escape after each error and display the correct message. Currently it just escapes.
I have a list of possible errors per action - how is it possible to read these and format as a flash message?
For example, the following error:
CreateSend::BadRequest: The CreateSend API responded with the following error - 172: You can create a maximum of five (5) clients per thirty (30) minutes
I currently have this in my controller:
def create_send_setup
...
begin
client = CreateSend::Client.create(#user.company_name, #user.username, #user.email, "(GMT) Dublin, Edinburgh, Lisbon, London", #user.country)
rescue
flash[:notice] = "Uh oh, there's been a problem. Please try again. Client"
end
begin
#client = CreateSend::Client.new(#user.createsend_client_api)
#client.set_access(#user.username, #password, #user.createsend_access_level)
#client.set_monthly_billing(#user.createsend_currency, true, #user.createsend_markup)
rescue
flash[:notice] = "Uh oh, there's been a problem. Please try again. Access"
end
flash[:notice] = "Great, now you just need to choose a plan"
redirect_to edit_user_registration_path
end
In a previous integration, I've used something like
case bla.code.to_s
when /2../
when '403'
raise "Could not update subscriber: new-customer-id is already in use."
...
end
What's the best way to extract the error code from the response and display as a flash message?
All you can do is work with the exception given-if there's no data payload you can try to parse the message strings, use it as-is, map all or part of it to your own messages, etc.
For example, if the example message you show is normalized, a regex could be used to grab the numeric and message portions:
[1] pry(main)> s = "CreateSend::BadRequest: The CreateSend API responded with the following error - 172: You can create a maximum of five (5) clients per thirty (30) minutes"
=> "CreateSend::BadRequest: The CreateSend API responded with the following error - 172: You can create a maximum of five (5) clients per thirty (30) minutes"
[2] pry(main)> md = s.match /.*?- (\d+): (.*)/
=> #<MatchData
"CreateSend::BadRequest: The CreateSend API responded with the following error - 172: You can create a maximum of five (5) clients per thirty (30) minutes"
1:"172"
2:"You can create a maximum of five (5) clients per thirty (30) minutes">
[3] pry(main)> md[1]
=> "172"
[4] pry(main)> md[2]
=> "You can create a maximum of five (5) clients per thirty (30) minutes"
As Dave said, if the exception is all you've got, you can only capture the exception and do something with that exception as a string.
For example:
begin
...
rescue Exception => ex
# here "ex.message" contains your string, you can do anything you want with it
# or parse as is:
flash[:notice] = ex.message
end
redirect_to edit_user_registration_path
UPDATE
If you combine my proposed solution with those put forward by Dave, you'd get something like this, with which you can use your own error strings:
begin
...
rescue Exception => ex
code = ex.message.match(/.*?- (\d+): (.*)/)[1]
case code
when '172'
flash[:notice] = 'Please wait 30 minutes to create more clients'
end
end
redirect_to edit_user_registration_path
The best way of handling errors in this specific situation is by specifically handing the errors which the library defines itself. createsend-ruby defines CreateSendError, BadRequest, and Unauthorized. You do not need to parse any strings.
Here is an example take from the README, of handling errors:
require 'createsend'
CreateSend.api_key 'your_api_key'
begin
cl = CreateSend::Client.new "4a397ccaaa55eb4e6aa1221e1e2d7122"
id = CreateSend::Campaign.create cl.client_id, "", "", "", "", "", "", "", [], []
puts "New campaign ID: #{id}"
rescue CreateSend::BadRequest => br
puts "Bad request error: #{br}"
puts "Error Code: #{br.data.Code}"
puts "Error Message: #{br.data.Message}"
rescue Exception => e
puts "Error: #{e}"
end
The data field always contains a Code and Message, which correspond to the status codes API documentation.
Related
I have been trying to implement the following in a Rails app but it seems not to do anything at all:
rescue Twilio::REST::RequestError => e
puts e.message
I notice it is only in the older docs for the twilio ruby gem, not in the newest ones. Is it no longer supported? Or do I need to change this code somehow?
I have tried to use the above when I get an invalid number or other error message from the Twilio API, but it doesn't work and just crashes the controller.
Here's an example of what I did:
rescue Twilio::REST::RequestError => e
flash[:error] = e.message
Well, it was my bad. I hadn't properly implemented the block. I did the following and it works now:
def create
begin
to_number = message_params
boot_twilio
sms = #client.messages.create(
from: ENV['TWILIO_NUMBER'],
to: to_number,
body: "..."
)
rescue Twilio::REST::RequestError => e
flash[:error] = "..."
else
flash[:notice] = "..."
end
redirect_to path
end
Twilio developer evangelist here.
If you are using the v5 release candidates then you will need to update the error class you are trying to catch.
The error raised in an HTTP request will be a Twilio::REST::RestException which inherits from the more generic Twilio::REST::TwilioException. See the error classes here: https://github.com/twilio/twilio-ruby/blob/next-gen/lib/twilio-ruby/framework/exception.rb
Note: I would not report the error message directly to the user. Those messages are intended for developers to better understand the issue.
Consider a simple method -
def my_method(users)
eligible_users = []
users.each do |u|
# Go to the next user unless they are eligible
next unless is_eligible?(u)
begin
update_user(u)
eligible_users << u
rescue
puts "Error occured"
# Prints some other stuff about error
next
end
end
end
A key feature of this method is that it loops through users but continues to the next user even if a given user throws an error.
If I were writing spec tests for this, I'd love to pass an array of 3 users and purposely have it error out on the first user. I can then check that the 2nd and 3rd were still correctly processed.
How would I go about raising an error on purpose for only one of the result sets?
I was thinking I could stub is_eligible? and return an error for one of the result and true for the remainder -
allow_any_instance_of(MyClass).to receive(:is_eligible?).and_return(
raise StandardError.new,
true,
true
)
As expected, that doesn't work. Any other approaches?
Thanks!
I can't exactly answer the question, but rather than doing a begin resuce thing,you can follow this approach,
Make the update_user return true or false.
Keep an array of users that falied to update.
return an object like
response: { status: "failure", message: "falied to update #{pluralize(failed_users_array.count, 'user')}", failures: failed_users_array.join(", ") } for failures and
response: { status: "success", message: "#{pluralize(users.count, 'user')} updated successfully" } for all success.
now you can easily test,
have two cases, one where you can test failures and when you can test all success.
Just stub the response object.
For raising errors, you have to do .and_raise("some tezt or StandardError.new") , thats in the docs.
Two things I want:
a) I want to be able to save a record in the db only if the API call succeeds
b) I want to execute the API call only if the db record saves successfully
The goal, is to keep data stored locally (in the DB) consistent with that of the data on Stripe.
#payment = Payment.new(...)
begin
Payment.transaction do
#payment.save!
stripe_customer = Stripe::Customer.retrieve(manager.customer_id)
charge = Stripe::Charge.create(
amount: #plan.amount_in_cents,
currency: 'usd',
customer: stripe_customer.id
)
end
# https://stripe.com/docs/api#errors
rescue Stripe::CardError, Stripe::InvalidRequestError, Stripe::APIError => error
#payment.errors.add :base, 'There was a problem processing your credit card. Please try again.'
render :new
rescue => error
render :new
else
redirect_to dashboard_root_path, notice: 'Thank you. Your payment is being processed.'
end
The above following will work, because if the record (on line 5) doesn't save, the rest of the code doesn't execute.
But what if I needed the #payment object saved after the API call, because I need to assign the #payment object with values from the API results. Take for example:
#payment = Payment.new(...)
begin
Payment.transaction do
stripe_customer = Stripe::Customer.retrieve(manager.customer_id)
charge = Stripe::Charge.create(
amount: #plan.amount_in_cents,
currency: 'usd',
customer: stripe_customer.id
)
#payment.payment_id = charge[:id]
#payment.activated_at = Time.now.utc
#payment.save!
end
# https://stripe.com/docs/api#errors
rescue Stripe::CardError, Stripe::InvalidRequestError, Stripe::APIError => error
#payment.errors.add :base, 'There was a problem processing your credit card. Please try again.'
render :new
rescue => error
render :new
else
redirect_to dashboard_root_path, notice: 'Thank you. Your payment is being processed.'
end
You notice #payment.save! happens after the API call. This could be a problem, because the API call ran, before the DB tried to save the record. Which could mean, a successful API call, but a failed DB commit.
Any ideas / suggestions for this scenario?
You can't execute API => DB and DB => API at the same time (sounds like an infinite execution conditions), at least I can't image how you can achieve this workflow. I understand your data consistency needs, so I propose:
Check if record is valid #payment.valid? (probably with a custom method like valid_without_payment?)
Run api call
Save record (with payment_id) only if api call succeds
Alternatively:
Save record without payment_id
Run api call
Update record with payment_id (api response) if call succeds
Run a task (script) periodically (cron) to check inconsistent instances (where(payment_id: nil)) and delete it
I think both options are acceptable and your data will remain consistent.
I created a batch email system for my website. The problem I have, which is terrible, is it continuously sends out emails. It seems the job is stuck in an infinite loop. Please advise. It is crazy because on my development server only one email is sent per account, but on my production server I received 5 emails. Thus, meaning all users of my site received multiple emails.
Controller:
class BatchEmailsController < ApplicationController
before_filter :authenticate_admin_user!
def deliver
flash[:notice] = "Email Being Delivered"
Delayed::Job.enqueue(BatchEmailJob.new(params[:batch_email_id]), 3, 10.seconds.from_now, :queue => 'batch-email', :attempts => 0)
redirect_to admin_batch_emails_path
end
end
Job in the lib folder:
class BatchEmailJob < Struct.new(:batch_email_id)
def perform
be = BatchEmail.find(batch_email_id)
if be.to.eql?("Contractors")
cs = Contractor.all
cs.each do|c|
begin
BatchEmailMailer.batch_email(be.subject, be.message, be.link_name, be.link_path, be.to, c.id).deliver
rescue Exception => e
Rails.logger.warn "Batch Email Error: #{e.message}"
end
else
ps = Painter.all
ps.each do |p|
begin
BatchEmailMailer.batch_email(be.subject, be.message, be.link_name, be.link_path, be.to, p.id).deliver
rescue Exception => e
Rails.logger.warn "Batch Email Error: #{e.message}"
end
end
end
end
end
Delayed Job Initializer:
Delayed::Worker.max_attempts = 0
Please provide feedback on this approach. I want to send out the batch email to all users, but avoid retrying multiple times if something goes wrong. I added rescue block to catch email exceptions in hope that the batch will skip errors and continue processing. As a last resort do not run again if something else goes wrong.
What one of my apps does which seems to work flawlessly after millions of emails:
1) in an initializer, do NOT let DelayedJob re-attempt a failed job AND ALSO not let DJ delete failed jobs:
Delayed::Worker.destroy_failed_jobs = false
Delayed::Worker.max_attempts = 1
2) Scheduling a mass email is 1 job, aka the "master job"
3) When THAT jobs runs, it spawns N jobs where N is the number of emails being sent. So each email gets its own job. (Note: if you use a production email service with 'batch' capability, one "email" might actually be a batch of 100 or 1000 emails.)
4) We have an admin panel that shows us if we have any failed jobs, and if they are, because we don't delete them, we can inspect the failed job and see what happened (malformed email address etc)
If one email fails, the others are un-affected. And no email can ever be sent twice.
I'm using the Mailman gem to process incoming email for my Rails app. My application looks for a YAML document in the plain-text email and then loads it into a Ruby object for further manipulation by the app.
However, I want to be able to plan ahead for email clients that might respond with a multi-part email. I need to get the plain-text part of the email and pass it into the YAML parser.
For some reason, it's still having problems parsing the YAML. I'm guessing because it's not really getting the plain text part here.
Is there a better way to get the text/plain part of an email with Mailman? Should I scrap Mailman and just get down and dirty with ActionMailer instead?
Mailman::Application.run do
default do
begin
message.parts.each do |part|
Mailman.logger.info part.content_type
if part.content_type == 'text/plain; charset=ISO-8859-1' # My poor way of getting the text part
the_yaml = part.body.decoded.scan(/(\-\-\-.*\.\.\.)/m).first.last # Find the YAML doc in the email and assign it to the_yaml
ruby_obj = YAML::load(the_yaml.sub(">", "")) # Remove any >'s automatically added by email clients
if ruby_obj['Jackpots']
ruby_obj['Jackpots'].each do |jackpot|
jp = Jackpot.find(jackpot['jackpot']['id'])
jp.prize = jackpot['jackpot']['prize']
jp.save
end
end
end
end
rescue Exception => e
Mailman.logger.error "Exception occurred while receiving message:\n#{message}"
Mailman.logger.error [e, *e.backtrace].join("\n")
end
end
end
I was able to find a little bit better way to handle getting the text part of the email.
Mailman::Application.run do
default do
begin
if message.multipart?
the_message = message.text_part.body.decoded
else
the_message = message.body.decoded
end
the_yaml = the_message.sub(">", "").scan(/(\-\-\-.*\.\.\.)/m).first.last
ruby_obj = YAML::load(the_yaml)
if ruby_obj['Jackpots']
ruby_obj['Jackpots'].each do |jackpot|
jp = Jackpot.find(jackpot['jackpot']['id'])
jp.prize = jackpot['jackpot']['prize']
jp.save
end
end
rescue Exception => e
Mailman.logger.error "Exception occurred while receiving message:\n#{message}"
Mailman.logger.error [e, *e.backtrace].join("\n")
end
end
end
And then after running it through the debugger and inspecting after the text part was successfully parsed. It would get hung up on the YAML loading. Turns out, a couple of my lines were too long, to the email client inserted a newline, breaking a comment in my YAML, and thus breaking the whole YAML document.