I'm streaming data using the following approach:
self.response_body = Enumerator.new do |y|
10_000_000.times do |i|
y << "This is line #{i}\n"
end
end
I'm trying to catch any exception generated inside Enumerator and present something nicer to the user. Right now, the app is presenting an ugly error page from Torquebox. e.g.
.
I tried rescue and redirect_to and many other ways to catch the exception (including add a middleware class for handling exceptions). Any help would be appreciated!.
(The app is made under jruby v1.7.19 and torquebox v3.1.1)
I have partial solution for this issue:
Check the external file headers. The app is sending a header request to the external server.
1.1 If the status is 200 (success), then the app starts the retrieve process as before.
1.2 If the status is any other value, then an error page is displayed with the appropriate error message.
And the code looks like this:
uri = URI.parse(link)
net_http = Net::HTTP.new(uri.host, uri.port)
net_http.open_timeout = options[:timeout]
net_http.read_timeout = options[:timeout]
net_http.use_ssl = (uri.scheme == 'https')
if net_http.use_ssl?
net_http.cert = OpenSSL::X509::Certificate.new(File.read(options[:cert]))
net_http.key = OpenSSL::PKey::RSA.new(File.read(options[:cert]))
net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
begin
request = Net::HTTP::Get.new(uri.request_uri)
res = net_http.request(request)
if res.code.to_i == 200
self.response.headers['Pragma'] = 'no-cache'
self.response.headers['Cache-Control'] = 'private,max-age=0,must-revalidate,no-store'
self.response.headers['Last-Modified'] = Time.now.ctime.to_s
self.response.headers['Accept-Ranges'] = 'bytes'
self.response.headers['Content-Type'] = type
self.response.headers['Content-Disposition'] = "attachment; filename=\"#{save_as}\""
self.response.headers['Content-Transfer-Encoding'] = 'binary'
self.response.headers['Content-Description'] = 'File Transfer'
self.response.headers['Content-Length'] = "#{filesize}"
self.response_body = FileStreamer.new(link, options)
else
raise "The requested file is not available at the moment"
end
rescue SocketError => se
raise 'It seems the URL is not correct'
rescue Errno::ECONNREFUSED => cr
raise 'It seems the server refused the connection request'
rescue Errno::EHOSTUNREACH => hu
raise 'It seems the server cannot be reached'
rescue Errno::ENETUNREACH => nu
raise 'It seems the network cannot be reached'
rescue Timeout::Error => te
raise 'Request timed out while connecting to the server'
rescue StandardError => e
raise e.message
end
The FileStreamer is a rack middleware which response to each method.
def each
begin
#net_http.start do |http|
http.request_get(#uri.request_uri(),
{'User-Agent' => 'FileStreamer'}) do |response|
case response
when Net::HTTPSuccess then
response.read_body do |segment|
yield segment
end
when Net::HTTPRedirection then
raise 'Redirection not allowed'
else
raise "Error trying to retrieve a file:\n" \
" URL: #{#uri.to_s}\n" \
" Error: #{response.message.to_s}"
end
end
end
rescue SocketError => se
raise 'It seems the URL is not correct'
rescue Errno::ECONNREFUSED => cr
raise 'It seems the server refused the connection request'
rescue Errno::EHOSTUNREACH => hu
raise 'It seems the server cannot be reached'
rescue Errno::ENETUNREACH => nu
raise 'It seems the network cannot be reached'
rescue Timeout::Error => te
raise 'Request timed out while connecting to the server'
rescue StandardError => e
raise e.message
end
end
Keep in mind this solution is not including errors happening in the middle of the process, I guess if an error happens in the middle, then the app will display the ugly error page.
Related
I'm doing a stub request to File but since I call Tempfile (that is a subclass of File) before calling File, Tempfile is intercepting the stub I define.
Model:
def download_file
#...
begin
tempfile = Tempfile.new([upload_file_name, '.csv'])
File.open(tempfile, 'wb') { |f| f.write(result.body.to_s) }
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
end
Rspec example:
it 'could not write to tempfile, so the report is created but without content' do
allow(File).to receive(:open).and_return Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
Error:
tempfile in the line
tempfile = Tempfile.new([upload_file_name, '.csv'])
receives Errno::ENOENT, indicating that the stub is going to Tempfile instead of to File.
How can I define the stub to go to File instead of to Tempfile?
There's no need to reopen a Tempfile, it's already open and delegates to File.
def download_file
tempfile = Tempfile.new([upload_file_name, '.csv'])
tempfile.write(result.body.to_s)
tempfile.path
# A method has an implicit begin.
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Then you can mock just Tempfile.new. Note that exceptions are raised, not returned.
it 'could not write to tempfile, so the report is created but without content' do
# Exceptions are raised, not returned.
allow(Tempfile).to receive(:new)
.and_raise Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
However, this remains fragile glass-box testing. Your test has knowledge of the implementation, if the implementation changes the test gives a false negative. And it still has to hope mocking Tempfile.new doesn't break something else.
Instead, extract temp file creation from download_file.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Now the mocking can be targeted to a specific method in a specific object. And we can apply some good rspec patterns.
context 'when the Tempfile cannot be created' do
# Here I'm assuming download_file is part of the Controller being tested.
before do
allow(#controller).to receive(:new_temp_file_for_upload)
.and_raise Errno::ENOENT
end
it 'creates the report without content' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:success)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to be nil
end
end
Note: returning "success" and an empty report after an internal failure is probably incorrect. It should return a 5xx error so the user knows there was a failure without having to look at the content.
download_file is doing too many things. It's both downloading a file and deciding what to do with a specific error. It should just download the file. Let something higher up in the call stack decide what to do with the exception. Methods which do one thing are simpler and more flexible and easier to test and less buggy.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
end
context 'when the download fails' do
before do
allow(#controller).to receive(:download_file)
.and_raise "krunch!"
end
it 'responds with an error' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:error)
end
end
Note that no specific error is needed. It's enough that download_file raises an exception. This test now has no knowledge of the internals beyond knowing that download_file is called.
I'm working with an Api, so far so good i am able to send requests and receive response, but i want the ruby code to send the json response and message that can be accessible from my rails controller so that i can get simple_form to render the error or success messages and also handle redirection from the rails controller, i also want to save transaction details from the response in a model, here's my lib code
class SimplePay
require 'net/https'
require 'uri'
require 'json'
def self.pay(api_key, token, amount, amount_currency, status)
uri = URI.parse('https://checkout.simplepay.ng/v2/payments/card/charge/')
header = {'Content-Type': 'text/json'}
data = {
'token': token,
'amount': amount,
'amount_currency': amount_currency
}
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri, header)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(uri.request_uri, header)
request.basic_auth(api_key, '')
request.set_form_data(data)
response = http.request(request)
res = JSON.parse(response.read_body)
response_code = res['response_code']
if response.kind_of? Net::HTTPSuccess
if response_code.eql? 20000
puts 'success'
else
puts 'Failed'
end
elsif status.eql? true
puts 'Success But Not Charged'
elsif status.eql? false
raise error 'Failed To Charge Card'
elsif response.kind_of? Net::HTTPBadRequest
raise error 'Failed 400'
elsif response.kind_of? Net::HTTPError
raise error 'Failed 404'
else
raise error 'Unknown Error'
end
end
end
How do i go about achieving this?
I would say send a Hash to the controller and then when you are sending it just doing a .to_json will make it into a json
I am using the following code to query maxmind for geolocation of a user's IP address. I want to make sure I am prepared for any errors/timeouts from maxmind's servers. Should I implement some type of rescue? If so, what is recommended?
uri = URI("https://geoip.maxmind.com/geoip/v2.1/city/#{request.remote_ip}?pretty")
Net::HTTP.start(uri.host, uri.port,
:use_ssl => uri.scheme == 'https',
:verify_mode => OpenSSL::SSL::VERIFY_NONE) do |http|
request = Net::HTTP::Get.new uri.request_uri
request.basic_auth 'USER_ID', 'KEY'
response = http.request request # Net::HTTPResponse object
if response.kind_of? Net::HTTPSuccess
location_hash = JSON.parse(response.body)
end
end
end
To rescue all exceptions:
begin
#your code
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
# do something with exception
end
You can also rescue single errors putting different rescues (use comma to rescue more than one at once):
begin
# your code
rescue Timeout::Error => e
rescue Errno::EINVAL => e
...
end
Even though I can capture the exception raised from #post.save and log the error and print the threads
begin
if #post.save
rescue Exception => ex
# rescue Errno::ECONNREFUSED => ex
Thread.list.each {|t| p t}
Rails.logger.error "ERROR: Could not save blog post! Is the Solr server up and running? Exception: #{ex}"
it still errors out on the web page and doesn't show any of my code in the stack trace. The solr Sunspot model callback is running on a separate thread.
rsolr (1.0.9) lib/rsolr/connection.rb:19:in `rescue in execute'
rsolr (1.0.9) lib/rsolr/connection.rb:14:in `execute'
...
sunspot_rails (2.1.0) lib/sunspot/rails/solr_instrumentation.rb:15:in `send_and_receive_with_as_instrumentation'
(eval):2:in `post'
rsolr (1.0.9) lib/rsolr/client.rb:67:in `update'
...
/usr/local/rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/webrick/httpserver.rb:138:in `service'
/usr/local/rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/webrick/httpserver.rb:94:in `run'
/usr/local/rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/webrick/server.rb:295:in `block in start_thread'
Notice block in start_thread?
So how can I capture this exception and display an error to the user? I don't think the whole app should crash just because Solr isn't running.
I found this, http://makandracards.com/makandra/5273-using-solr-with-sunspot, but it only describes how to capture search exceptions, not index/update exceptions.
Wow. After what seems like 20 hours... create lib/rsolr/connection.rb:
require 'net/http'
require 'net/https'
# The default/Net::Http adapter for RSolr.
class RSolr::Connection
# using the request_context hash,
# send a request,
# then return the standard rsolr response hash {:status, :body, :headers}
def execute client, request_context
h = http request_context[:uri], request_context[:proxy], request_context[:read_timeout], request_context[:open_timeout]
request = setup_raw_request request_context
request.body = request_context[:data] if request_context[:method] == :post and request_context[:data]
begin
response = h.request request
charset = response.type_params["charset"]
{:status => response.code.to_i, :headers => response.to_hash, :body => force_charset(response.body, charset)}
rescue Errno::ECONNREFUSED => e
Rails.logger.error "ERROR: #execute: Could not connect to Solr: #{e.message}"
# How to display an error message to the user?
# ActionController::Base.flash.now.alert "Could not connect to search indexer."
# Maybe http://stackoverflow.com/questions/393395/how-to-call-expire-fragment-from-rails-observer-model/608700#608700 ?
return nil
# raise(Errno::ECONNREFUSED.new(request_context.inspect))
# catch the undefined closed? exception -- this is a confirmed ruby bug
rescue NoMethodError
Rails.logger.error "ERROR: #execute: NoMethodError: Could not connect to Solr: #{e.message}"
return nil
# $!.message == "undefined method `closed?' for nil:NilClass" ?
# raise(Errno::ECONNREFUSED.new) :
# raise($!)
end
end
protected
# This returns a singleton of a Net::HTTP or Net::HTTP.Proxy request object.
def http uri, proxy = nil, read_timeout = nil, open_timeout = nil
#http ||= (
http = if proxy
proxy_user, proxy_pass = proxy.userinfo.split(/:/) if proxy.userinfo
Net::HTTP.Proxy(proxy.host, proxy.port, proxy_user, proxy_pass).new uri.host, uri.port
else
Net::HTTP.new uri.host, uri.port
end
http.use_ssl = uri.port == 443 || uri.instance_of?(URI::HTTPS)
http.read_timeout = read_timeout if read_timeout
http.open_timeout = open_timeout if open_timeout
http
)
end
#
def setup_raw_request request_context
http_method = case request_context[:method]
when :get
Net::HTTP::Get
when :post
Net::HTTP::Post
when :head
Net::HTTP::Head
else
raise "Only :get, :post and :head http method types are allowed."
end
headers = request_context[:headers] || {}
raw_request = http_method.new request_context[:uri].request_uri
raw_request.initialize_http_header headers
raw_request.basic_auth(request_context[:uri].user, request_context[:uri].password) if request_context[:uri].user && request_context[:uri].password
raw_request
end
private
def force_charset body, charset
return body unless charset and body.respond_to?(:force_encoding)
body.force_encoding(charset)
end
end
I have a Rails application that is posting to Facebook. I put a rescue to prevent the error of posting the same message twice. I would like to have my app just notify the user and move on, but I cannot seem to rescue this error.
This is my code:
begin
current_user.facebook.feed!(:message => 'THIS IS A TEST PLEASE IGNORE::Hello, Facebook!')
rescue FbGraph::Unauthorized
flash[:alert] = "Already Posted"
end
redirect_to show(#track)
The error I get with this code is:
OAuthException :: (#506) Duplicate status message
Why are rescuing from FbGraph::Unauthorized when you're getting an OAuthException error?
begin
current_user.facebook.feed!(:message => 'THIS IS A TEST PLEASE IGNORE::Hello, Facebook!')
rescue OAuthException
flash[:alert] = "Already Posted"
end
redirect_to show(#track)
Try:
begin
current_user.facebook.feed!(:message => 'THIS IS A TEST PLEASE IGNORE::Hello, Facebook!')
rescue => e
if(e.fb_error_type == "OAuthException"
flash[:alert] = "Already Posted"
end
end