class Twingly::HTTP::Client

Constants

DEFAULT_FOLLOW_REDIRECTS_LIMIT
DEFAULT_HTTP_OPEN_TIMEOUT
DEFAULT_HTTP_TIMEOUT
DEFAULT_MAX_URL_SIZE_BYTES
DEFAULT_NUMBER_OF_RETRIES
DEFAULT_RETRYABLE_EXCEPTIONS
DEFAULT_RETRY_INTERVAL
TIMEOUT_EXCEPTIONS

Attributes

follow_redirects[W]
follow_redirects_limit[RW]
http_open_timeout[W]
http_timeout[W]
logger[RW]
max_url_size_bytes[W]
number_of_retries[W]
on_retry_callback[W]
request_id[W]
retry_interval[W]
retryable_exceptions[RW]

Public Class Methods

new(base_user_agent:, logger: default_logger) click to toggle source
# File lib/twingly/http.rb, line 49
def initialize(base_user_agent:, logger: default_logger)
  @base_user_agent = base_user_agent
  @logger          = logger

  initialize_defaults
end

Public Instance Methods

get(url, params: {}, headers: {}) click to toggle source
# File lib/twingly/http.rb, line 56
def get(url, params: {}, headers: {})
  http_response_for(:get, url: url, params: params, headers: headers)
end
post(url, body:, headers: {}) click to toggle source
# File lib/twingly/http.rb, line 60
def post(url, body:, headers: {})
  http_response_for(:post, url: url, body: body, headers: headers)
end

Private Instance Methods

app_metadata() click to toggle source
# File lib/twingly/http.rb, line 192
def app_metadata
  {
    "dyno_id": Heroku.dyno_id,
    "release": Heroku.release_version,
    "git_head": Heroku.slug_commit,
  }
end
create_http_client() click to toggle source
# File lib/twingly/http.rb, line 134
def create_http_client # rubocop:disable Metrics/MethodLength
  Faraday.new do |faraday|
    faraday.request :url_size_limit,
                    max_size_bytes: @max_url_size_bytes
    faraday.request :retry,
                    max: @number_of_retries,
                    interval: @retry_interval,
                    exceptions: @retryable_exceptions,
                    methods: [], # empty [] forces Faraday to run retry_if
                    retry_if: retry_if
    faraday.response :logfmt_logger, @logger.dup,
                     headers: true,
                     bodies: true,
                     request_id: @request_id
    if @follow_redirects
      faraday.use FaradayMiddleware::FollowRedirects,
                  limit: @follow_redirects_limit
    end
    faraday.adapter Faraday.default_adapter
    faraday.headers[:user_agent] = user_agent
  end
end
default_headers() click to toggle source
# File lib/twingly/http.rb, line 200
def default_headers
  {
    "X-Request-Id": @request_id,
  }.delete_if { |_name, value| value.to_s.strip.empty? }
end
default_logger() click to toggle source
# File lib/twingly/http.rb, line 66
def default_logger
  Logger.new(File::NULL)
end
http_get_response(url:, params:, headers:) click to toggle source

rubocop:enable all

# File lib/twingly/http.rb, line 104
def http_get_response(url:, params:, headers:)
  binary_url = url.dup.force_encoding(Encoding::BINARY)
  http_client = create_http_client

  headers = default_headers.merge(headers)

  http_client.get do |request|
    request.url(binary_url)
    request.params.merge!(params)
    request.headers.merge!(headers)
    request.options.timeout = @http_timeout
    request.options.open_timeout = @http_open_timeout
  end
end
http_post_response(url:, body:, headers:) click to toggle source
# File lib/twingly/http.rb, line 119
def http_post_response(url:, body:, headers:)
  binary_url = url.dup.force_encoding(Encoding::BINARY)
  http_client = create_http_client

  headers = default_headers.merge(headers)

  http_client.post do |request|
    request.url(binary_url)
    request.headers.merge!(headers)
    request.body = body
    request.options.timeout = @http_timeout
    request.options.open_timeout = @http_open_timeout
  end
end
http_response_for(method, **args) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/twingly/http.rb, line 84
def http_response_for(method, **args)
  response = case method
             when :get
               http_get_response(**args)
             when :post
               http_post_response(**args)
             end

  Response.new(headers: response.headers.to_h,
               status: response.status,
               body: response.body)
rescue *(@retryable_exceptions + TIMEOUT_EXCEPTIONS)
  raise ConnectionError
rescue Faraday::UrlSizeLimit::LimitExceededError => error
  raise UrlSizeLimitExceededError, error.message
rescue FaradayMiddleware::RedirectLimitReached => error
  raise RedirectLimitReachedError, error.message
end
initialize_defaults() click to toggle source
# File lib/twingly/http.rb, line 70
def initialize_defaults
  @request_id             = nil
  @http_timeout           = DEFAULT_HTTP_TIMEOUT
  @http_open_timeout      = DEFAULT_HTTP_OPEN_TIMEOUT
  @retryable_exceptions   = DEFAULT_RETRYABLE_EXCEPTIONS
  @number_of_retries      = DEFAULT_NUMBER_OF_RETRIES
  @retry_interval         = DEFAULT_RETRY_INTERVAL
  @on_retry_callback      = nil
  @follow_redirects       = false
  @follow_redirects_limit = DEFAULT_FOLLOW_REDIRECTS_LIMIT
  @max_url_size_bytes     = DEFAULT_MAX_URL_SIZE_BYTES
end
retry_if() click to toggle source
# File lib/twingly/http.rb, line 157
def retry_if
  lambda do |env, exception|
    unwrapped_exception = unwrap_exception(exception)

    # we do not retry on timeouts due to our request time budget
    if timeout_error?(unwrapped_exception)
      false
    else
      @on_retry_callback&.call(env, unwrapped_exception)
      true
    end
  end
end
timeout_error?(error) click to toggle source
# File lib/twingly/http.rb, line 179
def timeout_error?(error)
  TIMEOUT_EXCEPTIONS.include?(error.class)
end
unwrap_exception(exception) click to toggle source
# File lib/twingly/http.rb, line 171
def unwrap_exception(exception)
  if exception.respond_to?(:wrapped_exception)
    exception.wrapped_exception
  else
    exception
  end
end
user_agent() click to toggle source
# File lib/twingly/http.rb, line 183
def user_agent
  format(
    "%<base>s (Release/%<release>s; Commit/%<commit>s)",
    base: @base_user_agent,
    release: Heroku.release_version,
    commit: Heroku.slug_commit
  )
end