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
|
# frozen_string_literal: true
require 'net/hippie/dns_cache'
require 'net/hippie/tls_parser'
module Net
module Hippie
# HTTP client with connection pooling, DNS caching, and retry logic.
class Client
include TlsParser
DEFAULT_HEADERS = {
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => "net/hippie #{VERSION}"
}.freeze
JITTER = Random.new.freeze
attr_reader :mapper, :logger, :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)
@default_headers = options.fetch(:headers, DEFAULT_HEADERS).freeze
configure_pool(options)
configure_tls(options)
end
%i[get post put patch delete].each do |method|
define_method(method) do |uri, headers: {}, body: {}, stream: false, &block|
run(uri, Net::HTTP.const_get(method.capitalize), headers, body, stream, &block)
end
end
%i[head options].each do |method|
define_method(method) do |uri, headers: {}, &block|
run(uri, Net::HTTP.const_get(method.capitalize), headers, {}, false, &block)
end
end
def execute(uri, request, limit: follow_redirects, stream: false, &block)
conn = connection_for(uri)
return conn.run(request) { |res| res.read_body(&block) } if stream && block
return execute_with_block(conn, request, &block) if block
response = conn.run(request)
limit.positive? && response.is_a?(Net::HTTPRedirection) ? follow_redirect(uri, response, limit) : response
end
def with_retry(retries: 3)
retries = [retries.to_i, 0].max
0.upto(retries) do |attempt|
return yield self
rescue *CONNECTION_ERRORS => error
raise if attempt == retries
sleep_with_backoff(attempt, retries, error)
end
end
def close_all
@pool.close_all
@dns_cache.clear
end
private
def configure_pool(options)
@dns_ttl = options.fetch(:dns_ttl, 300)
@dns_cache = DnsCache.new(
timeout: options.fetch(:dns_timeout, 5), ttl: @dns_ttl, logger: @logger
)
@pool = ConnectionPool.new(max_size: options.fetch(:max_connections, 100), dns_ttl: @dns_ttl)
end
def configure_tls(options)
@tls_cert = parse_cert(options[:certificate])
@tls_key = parse_key(options[:key], options[:passphrase])
@continue_timeout = options[:continue_timeout]
@ignore_eof = options.fetch(:ignore_eof, true)
end
def run(uri, method, headers, body, stream, &block)
uri = URI.parse(uri.to_s) unless uri.is_a?(URI::Generic)
execute(uri, build_request(method, uri, headers, body), stream: stream, &block)
end
def build_request(type, uri, headers, body)
merged = headers.empty? ? @default_headers : @default_headers.merge(headers)
path = uri.respond_to?(:request_uri) ? uri.request_uri : uri.path
type.new(path, merged).tap { |req| req.body = @mapper.map_from(merged, body) unless body.empty? }
end
def execute_with_block(conn, request, &block)
block.arity == 2 ? yield(request, conn.run(request)) : conn.run(request, &block)
end
def connection_for(uri)
uri = URI.parse(uri.to_s) unless uri.is_a?(URI::Generic)
key = [uri.scheme, uri.host, uri.port]
@pool.checkout(key) { Connection.new(uri.scheme, uri.host, uri.port, @dns_cache.resolve(uri.host), conn_opts) }
end
def sleep_with_backoff(attempt, max_retries, error)
delay = ((2**attempt) * 0.1) + JITTER.rand(0.05)
logger&.warn("[Hippie] #{error.class}: #{error.message} | Retry #{attempt + 1}/#{max_retries}")
sleep delay
end
def conn_opts
@options.merge(
tls_cert: @tls_cert, tls_key: @tls_key,
continue_timeout: @continue_timeout, ignore_eof: @ignore_eof
)
end
def follow_redirect(original_uri, response, limit)
redirect_uri = original_uri.merge(response['location'])
execute(redirect_uri, build_request(Net::HTTP::Get, redirect_uri, {}, {}), limit: limit - 1)
end
end
end
end
|