Rails 7 rescue API error and stil show the view - ruby-on-rails

In my Rails 7 app I'm using several 3rd parties API to provide fetch data. Each time I'm receiving an error I've to rescue with nil to still be able to display redirect user to desired page, e.g.:
# lib/custom_api.rb
module CustomApi
extend self
def fetch_transactions(user_id)
client.transactions.list(user_id:)
# rescue from custom error
rescue Errors::NotFoundError
nil
end
end
# transactions_controller.rb
class TransactionsController < ApplicationController
def index
transaction_list = CustomApi.fetch_transactions(current_user.id)
if transaction_list
#transactions = transaction_list
else
#transactions = transaction_list
flash[:alert] = 'No transactions'
end
end
end
# views/transactions/index.html.erb
<%= turbo_frame_tag 'transactions' do %>
<%= render partial: 'table_headers' %>
<%= render Transactions::TableComponent.new(records: #transactions) if #transactions %>
<% end %>
Everything works well but I've got 50 endpoints where I need to include rescue Errors::NotFoundError and I don't think it's super sufficient to to repeat this line 50 times. Is there a way to avoid that?

In general, using Rescuable is the Rails' way for rescuing from exception in a centralized manner.
Add this to your ApplicationController:
rescue_from Errors::NotFoundError, with: :handle_not_found_error_from_external_api
private
def handle_not_found_error_from_external_api
# handle the error in a generalized way, for example, by returning a response
# that renders a modal or a toast.
end
And remove these lines from your CustomApi:
# rescue from custom error
rescue Errors::NotFoundError
nil

Related

Exception for incorrect format in rails

Im trying to test if the format send through the request url is json or not?
so in link_to I sent the format like this
<%= link_to "Embed", {:controller=>'api/oembed' ,:action => 'show',:url => catalog_url, format: 'xml'} %>
In the relevant controller I catch the param and raise the exception like this
format_request = params[:format]
if format_request != "json"
raise DRI::Exceptions::NotImplemented
end
but the exception wont display instead the server simply ran into internal error but if I changed the param inside the controller then exception displayed so if the url is like this
<%= link_to "Embed", {:controller=>'api/oembed' ,:action => 'show',:url => catalog_url, format: 'json'} %>
format_request = "xml"
if format_request != "json"
raise DRI::Exceptions::NotImplemented
end
why 501 exception does not triggered if I send the format as xml in url? Im doing it for the testing purpose that in case if someone send the request with wrong format 501 expetion show up
Use ActionController::MimeResponds instead of badly reinventing the wheel:
# or whatever your base controller class is
class ApplicationController < ActionController::API
# MimeResponds is not included in ActionController::API
include ActionController::MimeResponds
# Defining this in your parent class avoids repeating the same error handling code
rescue_from ActionController::UnknownFormat do
raise DRI::Exceptions::NotImplemented # do you really need to add another layer of complexity?
end
end
module Api
class OembedController < ApplicationController
def oembed
respond_to :json
end
end
end
If you don't use respond_to Rails will implicitly assume that the controller responds to all response formats. But if you explicitly list the formats you respond to with a list of symbols (or the more common block syntax) Rails will raise ActionController::UnknownFormat if the request format is not listed. You can rescue exceptions with rescue_from which lets you use inheritance instead of repeating yourself with the same error handling.
As #max mentions, sending the format: 'xml' is unnecessary because Rails already knows the format of the request.
<%= link_to "Embed", {:controller=>'api/oembed' ,:action => 'show',:url => catalog_url } %>
In the controller:
def oembed
respond_to do |format|
format.json { # do the thing you want }
format.any(:xml, :html) { # render your DRI::Exceptions::NotImplemented }
end
end
Or if you want more control you could throw to a custom action:
def oembed
respond_to do |format|
format.json { # do the thing you want }
format.any(:xml, :html) { render not_implemented }
end
end
def not_implemented
# just suggestions here
flash[:notice] = 'No support for non-JSON requests at this time'
redirect_to return_path
# or if you really want to throw an error
raise DRI::Exceptions::NotImplemented
end
If you really want to reinvent the wheel (it's your wheel, reinvent if you want to):
I'd rename format to something else, it's probably reserved and might give you problems
<%= link_to "Embed", {:controller=>'api/oembed' ,:action => 'show',:url => catalog_url, custom_format: 'xml'} %>
Then, in your controller, you need to explicitly allow this parameter:
def oembed
raise DRI::Exceptions::NotImplemented unless format_params[:custom_format] == 'json'
end
private
def format_params
params.permit(:custom_format)
end

Passing Gibbon (Mailchimp) error from service to controller

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

How to write DRY flash messages for ajax requests?

What is the best (i.e, most maintainable, DRY) way to display a message to the user after an ajax request?
Obviously, the easiest way to achieve this for a single JS controller action is simply to use the associated JS partial. e.g.,
#create.js
$('.flash-container').html('<p>SUCCESS!</p>');
But, if the app contains a large number of ajaxed actions, this quickly becomes unmaintainable, requiring many partials to be updated if changes need to be made.
The approach I'm currently using is below. But this always seems very brittle and 'hacky' - I must be overlooking the Rails convention?
This is returning That page doesn't exist! the first time an ajax request is triggered on the page. All subsequent requests return the expected result until the page is reloaded. What is going on?
#my_controller.rb
def create
if #object.save
format.js { flash[:notice] = t('.notice') }
else
format.js { flash[:error] = t('.error') }
end
end
# application_controller.rb
after_action :flash_to_headers
def flash_to_headers
return unless request.xhr?
response.headers['X-Message'] = flash_message
response.headers["X-Message-Type"] = flash_type.to_s
flash.discard
end
def flash_message
[:alert, :error, :notice, :success].each do |type|
return flash[type] unless flash[type].blank?
end
return nil
end
def flash_type
[:alert, :error, :notice, :success].each do |type|
return type unless flash[type].blank?
end
return :empty
end
#flash.js.coffee
$(document).ajaxComplete (event, request) ->
msg = request.getResponseHeader("X-Message")
type = request.getResponseHeader("X-Message-Type")
if msg
alert(msg)
Why not change your js partial to a js.erb partial and embed the message you want to send before it is parsed into standard js instead of sending through the header?
in the same way you can have .html.erb files you can also have .js.erb files:
# create.js.erb
<% if flash[:notice] %>
$('.flash-container').html('<p><%= flash[:notice] %></p>');
<% end %>
in terms of keeping it maintainable and dry, that is just down to code design, create a shared js.erb partial that you render inside other .js.erb parials:
#shared/_ajax_messages.js.erb
<% if flash[:notice] %>
$('.flash-container').html('<p><%= flash[:notice] %></p>');
<% end %>
<% if flash[:error] %>
$('.flash-container').html('<p><%= flash[:error] %></p>');
<% end %>
# create.js.erb
<%= render(partial: 'ajax_messages') %>
create a shared helper_method in a top-level controller:
# some_controller.rb
helper_method :handle_ajax_messages
# create.js.erb
$('.flash-container').html('<p><%= handle_ajax_messages %></p>');
You could extract a ServiceObject handler, or a factory if your into DDD to create custom NoticeObjects or Value Objects with #to_html #to_js #to_json etc methods to keep things DRY for all types of requests. There are a huge variety of other options and ways to keep code DRY and maintainable if you break out of some so-called "Rails conventions".

Show detailed error info to some kind of user

On a rails 2.3 application I want to show the detailed error exception information if #user.is_admin? is true. Otherwise show the generic error on 500.rhtml.
What is the best way to approach this?
In your ApplicationController, rescue_from StandardError and in that method, render a view where you print out the exception message and backtrace.
class ApplicationController
rescue_from StandardError, :with => :show_exception
def show_exception(e)
#e = e
render "global/show_exception"
end
end
# app/views/global/show_exception.html.erb
<h1><%= #e.message %></h1>
<pre>
<%= #e.backtrace %>
</pre>
That should do the trick. The other option is to have a before_filter that checks to see if the user is an admin, and then sets the application configuration for consider_all_requests_local to true if so.
before_filter { Rails.application.config.consider_all_requests_local = current_user.try(:is_admin?) }

How would I get an error to display with this controller/model setup?

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.

Resources