1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
# frozen_string_literal: true
module Net
module Hippie
# A simple client for connecting with http resources.
class Client
DEFAULT_HEADERS = {
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => "net/hippie #{Net::Hippie::VERSION}"
}.freeze
attr_reader :mapper, :logger
attr_reader :follow_redirects
def initialize(options = {})
@options = options
@mapper = options.fetch(:mapper, ContentTypeMapper.new)
@logger = options.fetch(:logger, Net::Hippie.logger)
@follow_redirects = options.fetch(:follow_redirects, 0)
@http_connections = Hash.new do |hash, key|
uri = URI.parse(key.to_s)
build_http_for(uri).tap do |http|
hash[key] = http
end
end
end
def execute(uri, request, limit: follow_redirects, &block)
http = @http_connections[uri]
response = http.request(request)
if limit.positive? && response.is_a?(Net::HTTPRedirection)
url = build_url_for(http, response['location'])
request = request_for(Net::HTTP::Get, url)
execute(url, request, limit: limit - 1, &block)
else
block_given? ? yield(request, response) : response
end
end
def get(uri, headers: {}, body: {}, &block)
run(uri, Net::HTTP::Get, headers, body, &block)
end
def patch(uri, headers: {}, body: {}, &block)
run(uri, Net::HTTP::Patch, headers, body, &block)
end
def post(uri, headers: {}, body: {}, &block)
run(uri, Net::HTTP::Post, headers, body, &block)
end
def put(uri, headers: {}, body: {}, &block)
run(uri, Net::HTTP::Put, headers, body, &block)
end
def delete(uri, headers: {}, body: {}, &block)
run(uri, Net::HTTP::Delete, headers, body, &block)
end
# attempt 1 -> delay 0.1 second
# attempt 2 -> delay 0.2 second
# attempt 3 -> delay 0.4 second
# attempt 4 -> delay 0.8 second
# attempt 5 -> delay 1.6 second
# attempt 6 -> delay 3.2 second
# attempt 7 -> delay 6.4 second
# attempt 8 -> delay 12.8 second
def with_retry(retries: 3)
retries = 0 if retries.nil? || retries.negative?
0.upto(retries) do |n|
attempt(n, retries) do
return yield self
end
end
end
private
def default_headers
@options.fetch(:headers, DEFAULT_HEADERS)
end
def attempt(attempt, max)
yield
rescue *CONNECTION_ERRORS => error
raise error if attempt == max
delay = ((2**attempt) * 0.1) + Random.rand(0.05) # delay + jitter
logger.warn("`#{error.message}` #{attempt + 1}/#{max} Delay: #{delay}s")
sleep delay
end
def build_http_for(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = @options.fetch(:read_timeout, 10)
http.open_timeout = @options.fetch(:open_timeout, 10)
http.use_ssl = uri.scheme == 'https'
http.verify_mode = @options.fetch(:verify_mode, Net::Hippie.verify_mode)
http.set_debug_output(logger)
apply_client_tls_to(http)
http
end
def request_for(type, uri, headers: {}, body: {})
final_headers = default_headers.merge(headers)
type.new(URI.parse(uri.to_s), final_headers).tap do |x|
x.body = mapper.map_from(final_headers, body) unless body.empty?
end
end
def private_key(key, passphrase, type = OpenSSL::PKey::RSA)
passphrase ? type.new(key, passphrase) : type.new(key)
end
def apply_client_tls_to(http)
return if @options[:certificate].nil? || @options[:key].nil?
http.cert = OpenSSL::X509::Certificate.new(@options[:certificate])
http.key = private_key(@options[:key], @options[:passphrase])
end
def run(uri, http_method, headers, body, &block)
request = request_for(http_method, uri, headers: headers, body: body)
execute(uri, request, &block)
end
def build_url_for(http, path)
return path if path.start_with?('http')
"#{http.use_ssl? ? 'https' : 'http'}://#{http.address}#{path}"
end
end
end
end
|