Class: Blueticks::Transport

Inherits:
Object
  • Object
show all
Defined in:
lib/blueticks/transport.rb

Overview

HTTP transport for the Blueticks API. Wraps Net::HTTP with retry/backoff.

Constant Summary collapse

RETRIABLE_STATUS =
[429, 502, 503, 504].freeze
IDEMPOTENT_METHODS =
%w[GET HEAD OPTIONS DELETE PATCH PUT].freeze
BACKOFF_BASE =
0.5
BACKOFF_CAP =
8.0
RETRIABLE_NETWORK_ERRORS =
[
  Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
  Net::OpenTimeout, Net::ReadTimeout, SocketError, EOFError, IOError
].freeze
METHOD_CLASSES =
{
  "GET" => Net::HTTP::Get,
  "POST" => Net::HTTP::Post,
  "PUT" => Net::HTTP::Put,
  "PATCH" => Net::HTTP::Patch,
  "DELETE" => Net::HTTP::Delete,
  "HEAD" => Net::HTTP::Head
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, base_url:, timeout: 30.0, max_retries: 3, user_agent: nil, http: nil) ⇒ Transport

Returns a new instance of Transport.



31
32
33
34
35
36
37
38
39
# File 'lib/blueticks/transport.rb', line 31

def initialize(api_key:, base_url:, timeout: 30.0, max_retries: 3, user_agent: nil, http: nil)
  @api_key = api_key
  @base_url = base_url.sub(%r{/+\z}, "")
  @timeout = timeout
  @max_retries = max_retries
  @user_agent = "blueticks-ruby/#{Blueticks::VERSION}"
  @user_agent = "#{@user_agent} #{user_agent}" if user_agent && !user_agent.empty?
  @http = http # Optional injected client for testing.
end

Instance Method Details

#request(method, path, params: nil, body: nil, idempotency_key: nil) ⇒ Object

Issue a request. Returns the parsed JSON body, or nil on 204/empty.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/blueticks/transport.rb', line 42

def request(method, path, params: nil, body: nil, idempotency_key: nil)
  method = method.to_s.upcase
  uri = build_uri(path, params)
  is_idempotent = IDEMPOTENT_METHODS.include?(method) || !idempotency_key.nil?

  attempt = 0
  loop do
    req = build_request(method, uri, body: body, idempotency_key: idempotency_key)
    begin
      response = perform(uri, req)
    rescue *RETRIABLE_NETWORK_ERRORS => e
      if is_idempotent && attempt < @max_retries
        sleep_backoff(attempt, retry_after: nil)
        attempt += 1
        next
      end
      raise Errors::APIConnectionError.new(
        message: "connection error: #{e.class}: #{e.message}",
        status_code: nil,
        code: nil,
        request_id: nil
      )
    end

    return parse_success(response) if (200..299).cover?(response.code.to_i)

    retry_after = response.code.to_i == 429 ? parse_retry_after(response) : nil
    retriable = RETRIABLE_STATUS.include?(response.code.to_i)
    if retriable && is_idempotent && attempt < @max_retries
      sleep_backoff(attempt, retry_after: retry_after)
      attempt += 1
      next
    end

    raise build_api_error(response, retry_after: retry_after)
  end
end