Asana/ruby-asana

View on GitHub
lib/asana/http_client/error_handling.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require_relative '../errors'

module Asana
  class HttpClient
    # Internal: Handles errors from the API and re-raises them as proper
    # exceptions.
    module ErrorHandling
      include Errors

      module_function

      MAX_RETRIES = 5

      # Public: Perform a request handling any API errors correspondingly.
      #
      # request - [Proc] a block that will execute the request.
      #
      # Returns a [Faraday::Response] object.
      #
      # Raises [Asana::Errors::InvalidRequest] for invalid requests.
      # Raises [Asana::Errors::NotAuthorized] for unauthorized requests.
      # Raises [Asana::Errors::Forbidden] for forbidden requests.
      # Raises [Asana::Errors::NotFound] when a resource can't be found.
      # Raises [Asana::Errors::RateLimitEnforced] when the API is throttling.
      # Raises [Asana::Errors::ServerError] when there's a server problem.
      # Raises [Asana::Errors::APIError] when the API returns an unknown error.
      #
      # rubocop:disable Metrics/AbcSize
      def handle(num_retries = 0, &request)
        request.call
      rescue Faraday::ClientError => e
        raise e unless e.response

        case e.response[:status]
        when 400 then raise invalid_request(e.response)
        when 401 then raise not_authorized(e.response)
        when 402 then raise payment_required(e.response)
        when 403 then raise forbidden(e.response)
        when 404 then raise not_found(e.response)
        when 412 then recover_response(e.response)
        when 429 then raise rate_limit_enforced(e.response)
        when 500 then raise server_error(e.response)
        else raise api_error(e.response)
        end
      # Retry for timeouts or 500s from Asana
      rescue Faraday::ServerError => e
        raise server_error(e.response) unless num_retries < MAX_RETRIES

        handle(num_retries + 1, &request)
      rescue Net::ReadTimeout => e
        raise e unless num_retries < MAX_RETRIES

        handle(num_retries + 1, &request)
      end
      # rubocop:enable Metrics/AbcSize

      # Internal: Returns an InvalidRequest exception including a list of
      # errors.
      def invalid_request(response)
        errors = body(response).fetch('errors', []).map { |e| e['message'] }
        InvalidRequest.new(errors).tap do |exception|
          exception.response = response
        end
      end

      # Internal: Returns a NotAuthorized exception.
      def not_authorized(response)
        NotAuthorized.new.tap { |exception| exception.response = response }
      end

      # Internal: Returns a PremiumOnly exception.
      def payment_required(response)
        PremiumOnly.new.tap { |exception| exception.response = response }
      end

      # Internal: Returns a Forbidden exception.
      def forbidden(response)
        Forbidden.new.tap { |exception| exception.response = response }
      end

      # Internal: Returns a NotFound exception.
      def not_found(response)
        NotFound.new.tap { |exception| exception.response = response }
      end

      # Internal: Returns a RateLimitEnforced exception with a retry after
      # field.
      def rate_limit_enforced(response)
        retry_after_seconds = response[:headers]['Retry-After']
        RateLimitEnforced.new(retry_after_seconds).tap do |exception|
          exception.response = response
        end
      end

      # Internal: Returns a ServerError exception with a unique phrase.
      def server_error(response)
        phrase = body(response).fetch('errors', []).first['phrase']
        ServerError.new(phrase).tap do |exception|
          exception.response = response
        end
      end

      # Internal: Returns an APIError exception.
      def api_error(response)
        APIError.new.tap { |exception| exception.response = response }
      end

      # Internal: Parser a response body from JSON.
      def body(response)
        JSON.parse(response[:body])
      end

      # rubocop:disable Style/OpenStructUse
      def recover_response(response)
        r = response.dup.tap { |res| res[:body] = body(response) }
        Response.new(OpenStruct.new(env: OpenStruct.new(r)))
      end
      # rubocop:enable Style/OpenStructUse
    end
  end
end