lib/xeroizer/http.rb in xeroizer-2.15.5 vs lib/xeroizer/http.rb in xeroizer-2.15.6

- old
+ new

@@ -1,27 +1,28 @@ # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com> -# +# # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. -# +# # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. module Xeroizer module Http - + class BadResponse < StandardError; end + ACCEPT_MIME_MAP = { :pdf => 'application/pdf', :json => 'application/json' } - + # Shortcut method for #http_request with `method` = :get. # # @param [OAuth] client OAuth client # @param [String] url URL of request # @param [Hash] extra_params extra query string parameters. @@ -46,58 +47,62 @@ # @param [String] body XML message to put. # @param [Hash] extra_params extra query string parameters. def http_put(client, url, body, extra_params = {}) http_request(client, :put, url, body, extra_params) end - + private - + def http_request(client, method, url, body, params = {}) # headers = {'Accept-Encoding' => 'gzip, deflate'} - headers = { 'charset' => 'utf-8' } + headers = self.default_headers.merge({ 'charset' => 'utf-8' }) if method != :get headers['Content-Type'] ||= "application/x-www-form-urlencoded" end + content_type = params.delete(:content_type) + headers['Content-Type'] = content_type if content_type + # HAX. Xero completely misuse the If-Modified-Since HTTP header. headers['If-Modified-Since'] = params.delete(:ModifiedAfter).utc.strftime("%Y-%m-%dT%H:%M:%S") if params[:ModifiedAfter] - + # Allow 'Accept' header to be specified with :accept parameter. # Valid values are :pdf or :json. if params[:response] response_type = params.delete(:response) headers['Accept'] = case response_type when Symbol then ACCEPT_MIME_MAP[response_type] else response_type end end - + if params.any? url += "?" + params.map {|key,value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.join("&") end uri = URI.parse(url) attempts = 0 begin attempts += 1 - logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri} ") if self.logger + logger.info("XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri}") if self.logger + raw_body = params.delete(:raw_body) ? body : {:xml => body} + response = case method when :get then client.get(uri.request_uri, headers) - when :post then client.post(uri.request_uri, { :xml => body }, headers) - when :put then client.put(uri.request_uri, { :xml => body }, headers) + when :post then client.post(uri.request_uri, raw_body, headers) + when :put then client.put(uri.request_uri, raw_body, headers) end if self.logger - logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})") - + logger.info("XeroGateway Response (#{response.code})") unless response.code.to_i == 200 - logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body") + logger.info("#{uri.request_uri}\n== Response Body\n\n#{response.plain_body}\n== End Response Body") end end case response.code.to_i when 200 @@ -109,76 +114,81 @@ when 404 handle_object_not_found!(response, url) when 503 handle_oauth_error!(response) else - raise "Unknown response code: #{response.code.to_i}" + handle_unknown_response_error!(response) end rescue Xeroizer::OAuth::RateLimitExceeded if self.rate_limit_sleep raise if attempts > rate_limit_max_attempts - logger.info("== Rate limit exceeded, retrying") if self.logger + logger.info("Rate limit exceeded, retrying") if self.logger sleep_for(self.rate_limit_sleep) retry else raise end end end - + def handle_oauth_error!(response) error_details = CGI.parse(response.plain_body) description = error_details["oauth_problem_advice"].first - + # see http://oauth.pbworks.com/ProblemReporting # In addition to token_expired and token_rejected, Xero also returns # 'rate limit exceeded' when more than 60 requests have been made in # a second. case (error_details["oauth_problem"].first) - when "token_expired" then raise OAuth::TokenExpired.new(description) - when "token_rejected" then raise OAuth::TokenInvalid.new(description) - when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description) - else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description) + when "token_expired" then raise OAuth::TokenExpired.new(description) + when "token_rejected" then raise OAuth::TokenInvalid.new(description) + when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description) + when error_details["oauth_problem"] then raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description) + else raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.") end end - + def handle_error!(response, request_body) - + raw_response = response.plain_body - + # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing... # So let's ignore that :) raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', '' - + # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all) doc = Nokogiri::XML(raw_response) - + if doc && doc.root && doc.root.name == "ApiException" raise ApiException.new(doc.root.xpath("Type").text, doc.root.xpath("Message").text, raw_response, doc, request_body) else - - raise "Unparseable 400 Response: #{raw_response}" - + + raise BadResponse.new("Unparseable 400 Response: #{raw_response}") + end - + end - + def handle_object_not_found!(response, request_url) case(request_url) when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.") when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.") else raise ObjectNotFound.new(request_url) end end + def handle_unknown_response_error!(response) + raise BadResponse.new("Unknown response code: #{response.code.to_i}") + end + def sleep_for(seconds = 1) sleep seconds end - + end end