refactor of error handling

This commit is contained in:
stve 2016-01-27 12:51:45 -05:00
parent 298239a144
commit 3138d00398
6 changed files with 186 additions and 65 deletions

View file

@ -4,14 +4,32 @@ module Instapaper
# @return [Integer] # @return [Integer]
attr_reader :code attr_reader :code
ClientError = Class.new(self)
ServerError = Class.new(self)
ServiceUnavailableError = Class.new(self) ServiceUnavailableError = Class.new(self)
BookmarkError = Class.new(self) BookmarkError = Class.new(self)
FolderError = Class.new(self) FolderError = Class.new(self)
HighlightError = Class.new(self) HighlightError = Class.new(self)
OAuthError = Class.new(self) OAuthError = Class.new(self)
ERRORS = { CLIENT_ERRORS = {
401 => 'Unauthorized', 400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
}
SERVER_ERRORS = {
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
}
SERVICE_ERRORS = {
1040 => 'Rate-limit exceeded', 1040 => 'Rate-limit exceeded',
1041 => 'Premium account required', 1041 => 'Premium account required',
1042 => 'Application is suspended', 1042 => 'Application is suspended',
@ -43,29 +61,47 @@ module Instapaper
} }
CODES = [ CODES = [
ERRORS, CLIENT_ERRORS,
SERVER_ERRORS,
SERVICE_ERRORS,
BOOKMARK_ERRORS, BOOKMARK_ERRORS,
FOLDER_ERRORS, FOLDER_ERRORS,
HIGHLIGHT_ERRORS, HIGHLIGHT_ERRORS,
].collect(&:keys).flatten ].collect(&:keys).flatten
HTTP_ERRORS = CLIENT_ERRORS.merge(SERVER_ERRORS)
# Create a new error from an HTTP response # Create a new error from an HTTP response
# #
# @param response [HTTP::Response] # @param response [HTTP::Response]
# @return [Instapaper::Error] # @return [Instapaper::Error]
def self.from_response(code, path) def self.from_response(code, path)
if ERRORS.keys.include?(code) if (HTTP_ERRORS.keys + SERVICE_ERRORS.keys).include?(code)
new(ERRORS[code], code) from_response_code(code)
else else
case path case path
when /highlights/ then HighlightError.new(HIGHLIGHT_ERRORS[code], code) when /highlights/ then HighlightError.new(HIGHLIGHT_ERRORS[code], code)
when /bookmarks/ then BookmarkError.new(BOOKMARK_ERRORS[code], code) when /bookmarks/ then BookmarkError.new(BOOKMARK_ERRORS[code], code)
when /folders/ then FolderError.new(FOLDER_ERRORS[code], code) when /folders/ then FolderError.new(FOLDER_ERRORS[code], code)
else new('Unknown Error Code', code) else new('Unknown Error', code)
end end
end end
end end
# Create a new error from an HTTP response code
#
# @param code [Integer]
# @return [Instapaper::Error]
def self.from_response_code(code)
if CLIENT_ERRORS.keys.include?(code)
ClientError.new(CLIENT_ERRORS[code], code)
elsif SERVER_ERRORS.keys.include?(code)
ServerError.new(SERVER_ERRORS[code], code)
elsif SERVICE_ERRORS.keys.include?(code)
new(SERVICE_ERRORS[code], code)
end
end
# Initializes a new Error object # Initializes a new Error object
# #
# @param message [Exception, String] # @param message [Exception, String]

View file

@ -3,8 +3,8 @@ require 'http'
require 'json' require 'json'
require 'net/https' require 'net/https'
require 'openssl' require 'openssl'
require 'instapaper/error'
require 'instapaper/http/headers' require 'instapaper/http/headers'
require 'instapaper/http/response'
module Instapaper module Instapaper
module HTTP module HTTP
@ -27,61 +27,17 @@ module Instapaper
# @return [Array, Hash] # @return [Array, Hash]
def perform def perform
perform_request raw = @options.delete(:raw)
response = Instapaper::HTTP::Response.new(perform_request, path, raw)
response.valid? && response.body
end end
private private
def perform_request def perform_request
raw = @options.delete(:raw)
@headers = Instapaper::HTTP::Headers.new(@client, @request_method, @uri, @options).request_headers @headers = Instapaper::HTTP::Headers.new(@client, @request_method, @uri, @options).request_headers
options_key = @request_method == :get ? :params : :form options_key = @request_method == :get ? :params : :form
response = ::HTTP.headers(@headers).public_send(@request_method, @uri.to_s, options_key => @options) ::HTTP.headers(@headers).public_send(@request_method, @uri.to_s, options_key => @options)
fail_if_error(response, raw)
raw ? response.to_s : parsed_response(response)
end
def fail_if_error(response, raw)
fail_if_error_unparseable_response(response) unless raw
fail_if_error_in_body(parsed_response(response))
fail_if_error_response_code(response)
end
def fail_if_error_response_code(response)
return if response.status == 200
if Instapaper::Error::CODES.include?(response.status.code)
fail Instapaper::Error.from_response(response.status.code, @path)
else
fail Instapaper::Error::ServiceUnavailableError
end
end
def fail_if_error_unparseable_response(response)
response.parse(:json)
rescue JSON::ParserError
raise Instapaper::Error::ServiceUnavailableError
end
def fail_if_error_in_body(response)
error = error(response)
fail(error) if error
end
def error(response)
return unless response.is_a?(Array)
return unless response.size > 0
return unless response.first['type'] == 'error'
Instapaper::Error.from_response(response.first['error_code'], @path)
end
def parsed_response(response)
@parsed_response ||= begin
response.parse(:json)
rescue
response.body
end
end end
end end
end end

View file

@ -0,0 +1,62 @@
require 'instapaper/error'
module Instapaper
module HTTP
class Response
attr_reader :response, :raw_format, :path
def initialize(response, path, raw_format = false)
@response = response
@path = path
@raw_format = raw_format
end
def body
raw_format ? response.to_s : parsed
end
def valid?
!error?
end
def error?
fail_if_body_unparseable unless raw_format
fail_if_body_contains_error
fail_if_http_error
end
private
def parsed
@parsed_response ||= begin
response.parse(:json)
rescue
response.body
end
end
def fail_if_http_error
return if response.status.ok?
if Instapaper::Error::CODES.include?(response.status.code)
fail Instapaper::Error.from_response(response.status.code, path)
else
fail Instapaper::Error, 'Unknown Error'
end
end
def fail_if_body_unparseable
response.parse(:json)
rescue JSON::ParserError
raise Instapaper::Error::ServiceUnavailableError
end
def fail_if_body_contains_error
return unless parsed.is_a?(Array)
return unless parsed.size > 0
return unless parsed.first['type'] == 'error'
fail Instapaper::Error.from_response(parsed.first['error_code'], @path)
end
end
end
end

View file

@ -19,7 +19,33 @@ describe Instapaper::Error do
end end
end end
Instapaper::Error::ERRORS.each do |status, exception| Instapaper::Error::CLIENT_ERRORS.each do |status, exception|
context "when HTTP status is #{status}" do
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
before do
stub_post('/api/1.1/oauth/access_token')
.to_return(status: status, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
end
it "raises #{exception}" do
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error::ClientError)
end
end
end
Instapaper::Error::SERVER_ERRORS.each do |status, exception|
context "when HTTP status is #{status}" do
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
before do
stub_post('/api/1.1/oauth/access_token')
.to_return(status: status, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
end
it "raises #{exception}" do
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error::ServerError)
end
end
end
Instapaper::Error::SERVICE_ERRORS.each do |status, exception|
context "when HTTP status is #{status}" do context "when HTTP status is #{status}" do
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) } let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
before do before do
@ -71,13 +97,13 @@ describe Instapaper::Error do
end end
end end
context 'HTTP errors' do describe '.from_response' do
before do context 'with null path' do
stub_post('/api/1.1/oauth/access_token') it 'raises an Instapaper::Error' do
.to_return(status: 401, body: 'Unauthorized', headers: {}) error = Instapaper::Error.from_response(5000, nil)
end expect(error).to be_an Instapaper::Error
it 'raises an Instapaper::Error' do expect(error.message).to eq('Unknown Error')
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error) end
end end
end end
end end

View file

@ -9,8 +9,8 @@ describe Instapaper::HTTP::Request do
stub_post('/api/1.1/folders/list') stub_post('/api/1.1/folders/list')
.to_return(status: 503, body: '', headers: {content_type: 'application/json; charset=utf-8'}) .to_return(status: 503, body: '', headers: {content_type: 'application/json; charset=utf-8'})
end end
it 'raises a ServiceUnavailableError' do it 'raises a ServerError' do
expect { client.folders }.to raise_error(Instapaper::Error::ServiceUnavailableError) expect { client.folders }.to raise_error(Instapaper::Error::ServerError)
end end
end end

View file

@ -0,0 +1,41 @@
require 'spec_helper'
class FakeResponse
def initialize(body)
@body = body
end
def parse(_)
::JSON.parse(@body)
end
end
describe Instapaper::HTTP::Response do
describe '#body' do
context 'raw response' do
it 'returns the response in raw text' do
resp = Instapaper::HTTP::Response.new('foo', '', true)
expect(resp.body).to eq('foo')
end
end
context 'regular response' do
let(:fake_response) { FakeResponse.new('{"foo":"bar"}') }
it 'returns the parsed response' do
resp = Instapaper::HTTP::Response.new(fake_response, '')
expect(resp.body).to be_a(Hash)
end
end
end
describe '#valid?' do
context 'when http error' do
end
context 'when body unparseable' do
end
context 'when error in body' do
end
end
end