# frozen_string_literal: true require 'addressable/uri' require 'retryable' require_relative '../exception' module Minty module Mixins module HTTPProxy attr_accessor :headers, :base_uri, :timeout, :retry_count DEFAULT_RETRIES = 3 MAX_ALLOWED_RETRIES = 10 MAX_REQUEST_RETRY_JITTER = 250 MAX_REQUEST_RETRY_DELAY = 1000 MIN_REQUEST_RETRY_DELAY = 250 BASE_DELAY = 100 %i[get post post_file put patch delete delete_with_body].each do |method| define_method(method) do |uri, body = {}, extra_headers = {}| body = body.delete_if { |_, v| v.nil? } token = get_token authorization_header(token) unless token.nil? request_with_retry(method, uri, body, extra_headers) end end def retry_options sleep_timer = lambda do |attempt| wait = BASE_DELAY * (2**attempt - 1) wait += rand(wait + 1..wait + MAX_REQUEST_RETRY_JITTER) wait = [MAX_REQUEST_RETRY_DELAY, wait].min wait = [MIN_REQUEST_RETRY_DELAY, wait].max wait / 1000.to_f.round(2) end tries = 1 + [Integer(retry_count || DEFAULT_RETRIES), MAX_ALLOWED_RETRIES].min { tries: tries, sleep: sleep_timer, on: Minty::RateLimitEncountered } end def encode_uri(uri) path = base_uri ? Addressable::URI.new(path: uri).normalized_path : Addressable::URI.escape(uri) url(path) end def url(path) "#{base_uri}#{path}" end def add_headers(h = {}) raise ArgumentError, 'Headers must be an object which responds to #to_hash' unless h.respond_to?(:to_hash) @headers ||= {} @headers.merge!(h.to_hash) end def safe_parse_json(body) JSON.parse(body.to_s) rescue JSON::ParserError body end def request_with_retry(method, uri, body = {}, extra_headers = {}) Retryable.retryable(retry_options) do request(method, uri, body, extra_headers) end end def request(method, uri, body = {}, extra_headers = {}) result = case method when :get @headers ||= {} get_headers = @headers.merge({ params: body }).merge(extra_headers) call(:get, encode_uri(uri), timeout, get_headers) when :delete @headers ||= {} delete_headers = @headers.merge({ params: body }) call(:delete, encode_uri(uri), timeout, delete_headers) when :delete_with_body call(:delete, encode_uri(uri), timeout, headers, body.to_json) when :post_file body.merge!(multipart: true) post_file_headers = headers.slice(*headers.keys - ['Content-Type']) call(:post, encode_uri(uri), timeout, post_file_headers, body) else call(method, encode_uri(uri), timeout, headers, body.to_json) end case result.code when 200...226 then safe_parse_json(result.body) when 400 then raise Minty::BadRequest.new(result.body, code: result.code, headers: result.headers) when 401 then raise Minty::Unauthorized.new(result.body, code: result.code, headers: result.headers) when 403 then raise Minty::AccessDenied.new(result.body, code: result.code, headers: result.headers) when 404 then raise Minty::NotFound.new(result.body, code: result.code, headers: result.headers) when 429 then raise Minty::RateLimitEncountered.new(result.body, code: result.code, headers: result.headers) when 500 then raise Minty::ServerError.new(result.body, code: result.code, headers: result.headers) else raise Minty::Unsupported.new(result.body, code: result.code, headers: result.headers) end end def call(method, url, timeout, headers, body = nil) RestClient::Request.execute( method: method, url: url, timeout: timeout, headers: headers, payload: body ) rescue RestClient::Exception => e case e when RestClient::RequestTimeout raise Minty::RequestTimeout, e.message else e.response end end end end end