hearth 1.0.0.pre1 → 1.0.0.pre3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -4
- data/VERSION +1 -1
- data/lib/hearth/anonymous_auth_resolver.rb +11 -0
- data/lib/hearth/api_error.rb +15 -1
- data/lib/hearth/auth_option.rb +21 -0
- data/lib/hearth/auth_schemes/anonymous.rb +21 -0
- data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
- data/lib/hearth/auth_schemes/http_basic.rb +16 -0
- data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
- data/lib/hearth/auth_schemes/http_digest.rb +16 -0
- data/lib/hearth/auth_schemes.rb +32 -0
- data/lib/hearth/checksums.rb +31 -0
- data/lib/hearth/client.rb +66 -0
- data/lib/hearth/client_stubs.rb +128 -0
- data/lib/hearth/config/env_provider.rb +53 -0
- data/lib/hearth/config/resolver.rb +53 -0
- data/lib/hearth/configuration.rb +15 -0
- data/lib/hearth/connection_pool.rb +77 -0
- data/lib/hearth/context.rb +29 -4
- data/lib/hearth/dns/host_address.rb +27 -0
- data/lib/hearth/dns/host_resolver.rb +92 -0
- data/lib/hearth/dns.rb +48 -0
- data/lib/hearth/endpoint_rules.rb +154 -0
- data/lib/hearth/http/api_error.rb +4 -8
- data/lib/hearth/http/client.rb +206 -59
- data/lib/hearth/http/error_inspector.rb +85 -0
- data/lib/hearth/http/error_parser.rb +18 -20
- data/lib/hearth/http/field.rb +49 -0
- data/lib/hearth/http/fields.rb +117 -0
- data/lib/hearth/http/header_list_builder.rb +42 -0
- data/lib/hearth/http/header_list_parser.rb +92 -0
- data/lib/hearth/http/middleware/content_length.rb +7 -4
- data/lib/hearth/http/middleware/content_md5.rb +30 -0
- data/lib/hearth/http/middleware/request_compression.rb +154 -0
- data/lib/hearth/http/middleware.rb +12 -0
- data/lib/hearth/http/networking_error.rb +1 -14
- data/lib/hearth/http/request.rb +83 -56
- data/lib/hearth/http/response.rb +42 -13
- data/lib/hearth/http.rb +16 -5
- data/lib/hearth/identities/anonymous.rb +8 -0
- data/lib/hearth/identities/http_api_key.rb +16 -0
- data/lib/hearth/identities/http_bearer.rb +16 -0
- data/lib/hearth/identities/http_login.rb +20 -0
- data/lib/hearth/identities.rb +21 -0
- data/lib/hearth/identity_provider.rb +17 -0
- data/lib/hearth/interceptor.rb +506 -0
- data/lib/hearth/interceptor_context.rb +40 -0
- data/lib/hearth/interceptor_list.rb +48 -0
- data/lib/hearth/interceptors.rb +76 -0
- data/lib/hearth/json.rb +4 -4
- data/lib/hearth/middleware/auth.rb +103 -0
- data/lib/hearth/middleware/build.rb +32 -1
- data/lib/hearth/middleware/endpoint.rb +79 -0
- data/lib/hearth/middleware/host_prefix.rb +11 -8
- data/lib/hearth/middleware/initialize.rb +57 -0
- data/lib/hearth/middleware/parse.rb +45 -7
- data/lib/hearth/middleware/retry.rb +105 -24
- data/lib/hearth/middleware/send.rb +137 -26
- data/lib/hearth/middleware/sign.rb +65 -0
- data/lib/hearth/middleware/validate.rb +11 -1
- data/lib/hearth/middleware.rb +20 -8
- data/lib/hearth/middleware_stack.rb +2 -44
- data/lib/hearth/networking_error.rb +18 -0
- data/lib/hearth/number_helper.rb +3 -3
- data/lib/hearth/output.rb +8 -4
- data/lib/hearth/plugin_list.rb +53 -0
- data/lib/hearth/query/param.rb +56 -0
- data/lib/hearth/query/param_list.rb +54 -0
- data/lib/hearth/query/param_matcher.rb +31 -0
- data/lib/hearth/refreshing_identity_provider.rb +63 -0
- data/lib/hearth/request.rb +22 -0
- data/lib/hearth/response.rb +36 -0
- data/lib/hearth/retry/adaptive.rb +60 -0
- data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
- data/lib/hearth/retry/client_rate_limiter.rb +145 -0
- data/lib/hearth/retry/exponential_backoff.rb +15 -0
- data/lib/hearth/retry/retry_quota.rb +56 -0
- data/lib/hearth/retry/standard.rb +46 -0
- data/lib/hearth/retry.rb +29 -0
- data/lib/hearth/signers/anonymous.rb +16 -0
- data/lib/hearth/signers/http_api_key.rb +29 -0
- data/lib/hearth/signers/http_basic.rb +23 -0
- data/lib/hearth/signers/http_bearer.rb +19 -0
- data/lib/hearth/signers/http_digest.rb +19 -0
- data/lib/hearth/signers.rb +23 -0
- data/lib/hearth/structure.rb +7 -3
- data/lib/hearth/stubs.rb +38 -0
- data/lib/hearth/time_helper.rb +6 -5
- data/lib/hearth/validator.rb +60 -5
- data/lib/hearth/waiters/poller.rb +10 -9
- data/lib/hearth/waiters/waiter.rb +23 -9
- data/lib/hearth/xml/formatter.rb +11 -2
- data/lib/hearth/xml/node.rb +2 -3
- data/lib/hearth/xml/node_matcher.rb +0 -1
- data/lib/hearth.rb +37 -6
- data/sig/lib/hearth/aliases.rbs +6 -0
- data/sig/lib/hearth/anonymous_auth_resolver.rbs +5 -0
- data/sig/lib/hearth/api_error.rbs +13 -0
- data/sig/lib/hearth/auth_option.rbs +11 -0
- data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
- data/sig/lib/hearth/auth_schemes.rbs +13 -0
- data/sig/lib/hearth/block_io.rbs +9 -0
- data/sig/lib/hearth/client.rbs +9 -0
- data/sig/lib/hearth/client_stubs.rbs +5 -0
- data/sig/lib/hearth/configuration.rbs +7 -0
- data/sig/lib/hearth/dns/host_address.rbs +11 -0
- data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
- data/sig/lib/hearth/endpoint_rules.rbs +17 -0
- data/sig/lib/hearth/http/api_error.rbs +13 -0
- data/sig/lib/hearth/http/client.rbs +9 -0
- data/sig/lib/hearth/http/field.rbs +19 -0
- data/sig/lib/hearth/http/fields.rbs +43 -0
- data/sig/lib/hearth/http/header_list_builder.rbs +15 -0
- data/sig/lib/hearth/http/header_list_parser.rbs +19 -0
- data/sig/lib/hearth/http/networking_error.rbs +6 -0
- data/sig/lib/hearth/http/request.rbs +25 -0
- data/sig/lib/hearth/http/response.rbs +21 -0
- data/sig/lib/hearth/identities/anonymous.rbs +6 -0
- data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
- data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
- data/sig/lib/hearth/identities/http_login.rbs +11 -0
- data/sig/lib/hearth/identities.rbs +9 -0
- data/sig/lib/hearth/identity_provider.rbs +7 -0
- data/sig/lib/hearth/interceptor.rbs +9 -0
- data/sig/lib/hearth/interceptor_context.rbs +17 -0
- data/sig/lib/hearth/interceptor_list.rbs +16 -0
- data/sig/lib/hearth/interfaces.rbs +87 -0
- data/sig/lib/hearth/json/parse_error.rbs +9 -0
- data/sig/lib/hearth/networking_error.rbs +7 -0
- data/sig/lib/hearth/output.rbs +11 -0
- data/sig/lib/hearth/plugin_list.rbs +13 -0
- data/sig/lib/hearth/query/param.rbs +17 -0
- data/sig/lib/hearth/query/param_list.rbs +25 -0
- data/sig/lib/hearth/refreshing_identity_provider.rbs +10 -0
- data/sig/lib/hearth/request.rbs +9 -0
- data/sig/lib/hearth/response.rbs +11 -0
- data/sig/lib/hearth/retry/adaptive.rbs +13 -0
- data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
- data/sig/lib/hearth/retry/standard.rbs +13 -0
- data/sig/lib/hearth/retry/strategy.rbs +11 -0
- data/sig/lib/hearth/retry.rbs +9 -0
- data/sig/lib/hearth/signers/anonymous.rbs +9 -0
- data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
- data/sig/lib/hearth/signers/http_basic.rbs +9 -0
- data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
- data/sig/lib/hearth/signers/http_digest.rbs +9 -0
- data/sig/lib/hearth/signers.rbs +9 -0
- data/sig/lib/hearth/structure.rbs +6 -0
- data/sig/lib/hearth/stubs.rbs +9 -0
- data/sig/lib/hearth/union.rbs +5 -0
- data/sig/lib/hearth/waiters/waiter.rbs +17 -0
- data/sig/lib/hearth/xml/parse_error.rbs +9 -0
- metadata +151 -25
- data/lib/hearth/http/headers.rb +0 -70
- data/lib/hearth/middleware/around_handler.rb +0 -24
- data/lib/hearth/middleware/request_handler.rb +0 -24
- data/lib/hearth/middleware/response_handler.rb +0 -25
- data/lib/hearth/middleware_builder.rb +0 -246
- data/lib/hearth/stubbing/client_stubs.rb +0 -115
- data/lib/hearth/stubbing/stubs.rb +0 -32
- data/lib/hearth/waiters/errors.rb +0 -15
- data/sig/lib/seahorse/api_error.rbs +0 -10
- data/sig/lib/seahorse/document.rbs +0 -2
- data/sig/lib/seahorse/http/api_error.rbs +0 -21
- data/sig/lib/seahorse/http/headers.rbs +0 -47
- data/sig/lib/seahorse/http/response.rbs +0 -21
- data/sig/lib/seahorse/simple_delegator.rbs +0 -3
- data/sig/lib/seahorse/structure.rbs +0 -18
- data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
- data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
- data/sig/lib/seahorse/union.rbs +0 -6
data/lib/hearth/http/client.rb
CHANGED
@@ -1,67 +1,111 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'delegate'
|
3
4
|
require 'net/http'
|
4
5
|
require 'logger'
|
5
6
|
require 'openssl'
|
6
7
|
|
7
8
|
module Hearth
|
8
9
|
module HTTP
|
9
|
-
#
|
10
|
-
# @api private
|
10
|
+
# An HTTP client that uses Net::HTTP to send requests.
|
11
11
|
class Client
|
12
|
+
# @api private
|
13
|
+
OPTIONS = {
|
14
|
+
logger: nil,
|
15
|
+
debug_output: nil,
|
16
|
+
proxy: nil,
|
17
|
+
open_timeout: 15,
|
18
|
+
read_timeout: nil,
|
19
|
+
keep_alive_timeout: 5,
|
20
|
+
continue_timeout: 1,
|
21
|
+
write_timeout: nil,
|
22
|
+
ssl_timeout: nil,
|
23
|
+
verify_peer: true,
|
24
|
+
ca_file: nil,
|
25
|
+
ca_path: nil,
|
26
|
+
cert_store: nil,
|
27
|
+
host_resolver: nil
|
28
|
+
}.freeze
|
29
|
+
|
12
30
|
# Initialize an instance of this HTTP client.
|
13
31
|
#
|
14
32
|
# @param [Hash] options The options for this HTTP Client
|
15
33
|
#
|
16
|
-
# @option options [
|
17
|
-
#
|
34
|
+
# @option options [Logger] :logger (nil) A logger used to log Net::HTTP
|
35
|
+
# requests and responses when `:debug_output` is enabled.
|
18
36
|
#
|
19
|
-
# @option options [
|
37
|
+
# @option options [Boolean] :debug_output (false) When `true`,
|
38
|
+
# sets an output stream to the configured Logger (if any) for debugging.
|
20
39
|
#
|
21
|
-
# @option options [URI
|
40
|
+
# @option options [String, URI] :proxy A proxy to send
|
22
41
|
# requests through. Formatted like 'http://proxy.com:123'.
|
23
42
|
#
|
24
|
-
# @option options [
|
43
|
+
# @option options [Float] :open_timeout (15) Number of seconds to
|
44
|
+
# wait for the connection to open.
|
45
|
+
#
|
46
|
+
# @option options [Float] :read_timeout Number of seconds to wait
|
47
|
+
# for one block to be read (via one read(2) call).
|
48
|
+
#
|
49
|
+
# @option options [Float] :keep_alive_timeout (5) Seconds to reuse the
|
50
|
+
# connection of the previous request.
|
51
|
+
#
|
52
|
+
# @option options [Float] :continue_timeout (1) Seconds to wait for
|
53
|
+
# 100 Continue response.
|
54
|
+
#
|
55
|
+
# @option options [Float] :write_timeout Number of seconds to wait
|
56
|
+
# for one block to be written (via one write(2) call).
|
57
|
+
#
|
58
|
+
# @option options [Float] :ssl_timeout Sets the SSL timeout seconds.
|
59
|
+
#
|
60
|
+
# @option options [Boolean] :verify_peer (true) When `true`,
|
25
61
|
# SSL peer certificates are verified when establishing a
|
26
62
|
# connection.
|
27
63
|
#
|
28
|
-
# @option options [String] :
|
64
|
+
# @option options [String] :ca_file Full path to the SSL
|
29
65
|
# certificate authority bundle file that should be used when
|
30
66
|
# verifying peer certificates. If you do not pass
|
31
|
-
# `:
|
67
|
+
# `:ca_file` or `:ca_path` the system default
|
32
68
|
# will be used if available.
|
33
69
|
#
|
34
|
-
# @option options [String] :
|
70
|
+
# @option options [String] :ca_path Full path of the
|
35
71
|
# directory that contains the unbundled SSL certificate
|
36
72
|
# authority files for verifying peer certificates. If you do
|
37
|
-
# not pass `:
|
73
|
+
# not pass `:ca_file` or `:ca_path` the
|
38
74
|
# system default will be used if available.
|
75
|
+
#
|
76
|
+
# @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509
|
77
|
+
# certificate store that contains the SSL certificate authority.
|
78
|
+
#
|
79
|
+
# @option options [#resolve_address] :host_resolver
|
80
|
+
# An object, such as {Hearth::DNS::HostResolver} that responds to
|
81
|
+
# `#resolve_address`, returning an array of up to two IP addresses for
|
82
|
+
# the given hostname, one IPv6 and one IPv4, in that order.
|
83
|
+
# `#resolve_address` should take a nodename keyword argument and
|
84
|
+
# optionally other keyword args similar to Addrinfo.getaddrinfo's
|
85
|
+
# positional parameters.
|
39
86
|
def initialize(options = {})
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
87
|
+
unknown = options.keys - OPTIONS.keys
|
88
|
+
raise ArgumentError, "Unknown options: #{unknown}" unless unknown.empty?
|
89
|
+
|
90
|
+
OPTIONS.each_pair do |opt_name, default_value|
|
91
|
+
value = options.key?(opt_name) ? options[opt_name] : default_value
|
92
|
+
instance_variable_set("@#{opt_name}", value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
OPTIONS.each_key do |attr_name|
|
97
|
+
attr_reader(attr_name)
|
48
98
|
end
|
49
99
|
|
50
100
|
# @param [Request] request
|
51
101
|
# @param [Response] response
|
102
|
+
# @param [Logger] logger (nil)
|
52
103
|
# @return [Response]
|
53
|
-
def transmit(request:, response:)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
if uri.scheme == 'https'
|
59
|
-
configure_ssl(http)
|
60
|
-
else
|
61
|
-
http.use_ssl = false
|
104
|
+
def transmit(request:, response:, logger: nil)
|
105
|
+
net_request = build_net_request(request)
|
106
|
+
with_connection_pool(request.uri, logger) do |connection|
|
107
|
+
_transmit(connection, net_request, response)
|
62
108
|
end
|
63
|
-
|
64
|
-
_transmit(http, request, response)
|
65
109
|
response.body.rewind if response.body.respond_to?(:rewind)
|
66
110
|
response
|
67
111
|
rescue ArgumentError => e
|
@@ -73,37 +117,82 @@ module Hearth
|
|
73
117
|
|
74
118
|
private
|
75
119
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
120
|
+
def with_connection_pool(endpoint, logger)
|
121
|
+
pool = ConnectionPool.for(pool_config)
|
122
|
+
connection = pool.connection_for(endpoint) do
|
123
|
+
new_connection(endpoint, logger)
|
124
|
+
end
|
125
|
+
yield connection
|
126
|
+
pool.offer(endpoint, connection)
|
127
|
+
rescue StandardError => e
|
128
|
+
connection&.finish
|
129
|
+
raise e
|
130
|
+
end
|
131
|
+
|
132
|
+
# Starts and returns a new HTTP connection.
|
133
|
+
# @return [Net::HTTP]
|
134
|
+
def new_connection(endpoint, logger)
|
135
|
+
http = create_http(endpoint)
|
136
|
+
http.set_debug_output(@logger || logger) if @debug_output
|
137
|
+
configure_timeouts(http)
|
138
|
+
|
139
|
+
if endpoint.scheme == 'https'
|
140
|
+
configure_ssl(http)
|
141
|
+
else
|
142
|
+
http.use_ssl = false
|
143
|
+
end
|
144
|
+
|
145
|
+
http.start
|
146
|
+
http
|
147
|
+
end
|
148
|
+
|
149
|
+
def _transmit(http, net_request, response)
|
150
|
+
# Inform monkey patch to use our DNS resolver
|
151
|
+
Thread.current[:net_http_hearth_dns_resolver] = @host_resolver
|
152
|
+
http.request(net_request) do |net_resp|
|
153
|
+
unpack_response(net_resp, response)
|
85
154
|
end
|
155
|
+
ensure
|
156
|
+
# Restore the default DNS resolver
|
157
|
+
Thread.current[:net_http_hearth_dns_resolver] = nil
|
86
158
|
end
|
87
159
|
|
88
|
-
|
89
|
-
|
160
|
+
def unpack_response(net_resp, response)
|
161
|
+
response.status = net_resp.code.to_i
|
162
|
+
net_resp.each_header { |k, v| response.headers[k] = v }
|
163
|
+
net_resp.read_body do |chunk|
|
164
|
+
response.body.write(chunk)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Creates an HTTP connection to the endpoint.
|
169
|
+
# Applies proxy if set.
|
90
170
|
def create_http(endpoint)
|
91
171
|
args = []
|
92
172
|
args << endpoint.host
|
93
173
|
args << endpoint.port
|
94
|
-
args +=
|
174
|
+
args += proxy_parts if @proxy
|
95
175
|
# Net::HTTP.new uses positional arguments: host, port, proxy_args....
|
96
|
-
Net::HTTP.new(*args.compact)
|
176
|
+
HTTP.new(Net::HTTP.new(*args.compact))
|
177
|
+
end
|
178
|
+
|
179
|
+
def configure_timeouts(http)
|
180
|
+
http.open_timeout = @open_timeout
|
181
|
+
http.keep_alive_timeout = @keep_alive_timeout
|
182
|
+
http.read_timeout = @read_timeout
|
183
|
+
http.continue_timeout = @continue_timeout
|
184
|
+
http.write_timeout = @write_timeout
|
97
185
|
end
|
98
186
|
|
99
187
|
# applies ssl settings to the HTTP object
|
100
188
|
def configure_ssl(http)
|
101
189
|
http.use_ssl = true
|
102
|
-
|
190
|
+
http.ssl_timeout = @ssl_timeout
|
191
|
+
if @verify_peer
|
103
192
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
104
|
-
http.ca_file = @
|
105
|
-
http.ca_path = @
|
106
|
-
http.cert_store = @
|
193
|
+
http.ca_file = @ca_file
|
194
|
+
http.ca_path = @ca_path
|
195
|
+
http.cert_store = @cert_store
|
107
196
|
else
|
108
197
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
109
198
|
end
|
@@ -115,15 +204,28 @@ module Hearth
|
|
115
204
|
# @return [Net::HTTP::Request]
|
116
205
|
def build_net_request(request)
|
117
206
|
request_class = net_http_request_class(request)
|
118
|
-
req = request_class.new(request.
|
119
|
-
|
207
|
+
req = request_class.new(request.uri, net_headers_for(request))
|
208
|
+
|
209
|
+
# Net::HTTP adds a default Content-Type when a body is present.
|
210
|
+
# Set the body stream when it has an unknown size or when it is > 0.
|
211
|
+
# We instead add our own Content-Length header via Middleware.
|
212
|
+
if !request.body.respond_to?(:size) ||
|
213
|
+
(request.body.respond_to?(:size) && request.body.size.positive?)
|
214
|
+
req.body_stream = request.body
|
215
|
+
end
|
120
216
|
req
|
121
217
|
end
|
122
218
|
|
123
|
-
#
|
219
|
+
# Validate that fields are not trailers and return a hash of headers.
|
220
|
+
# @param [HTTP::Request] request
|
124
221
|
# @return [Hash<String, String>]
|
125
|
-
def
|
126
|
-
|
222
|
+
def net_headers_for(request)
|
223
|
+
# Trailers are not supported in Net::HTTP
|
224
|
+
if request.trailers.any?
|
225
|
+
raise NotImplementedError, 'Trailers are not supported in Net::HTTP'
|
226
|
+
end
|
227
|
+
|
228
|
+
request.headers.to_h
|
127
229
|
end
|
128
230
|
|
129
231
|
# @param [Http::Request] request
|
@@ -137,16 +239,61 @@ module Hearth
|
|
137
239
|
raise ArgumentError, msg
|
138
240
|
end
|
139
241
|
|
140
|
-
# Extract the parts of the
|
141
|
-
# @return [Array
|
142
|
-
def
|
242
|
+
# Extract the parts of the proxy URI
|
243
|
+
# @return [Array]
|
244
|
+
def proxy_parts
|
245
|
+
proxy = URI(@proxy)
|
143
246
|
[
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
247
|
+
proxy.host,
|
248
|
+
proxy.port,
|
249
|
+
proxy.user && CGI.unescape(proxy.user),
|
250
|
+
proxy.password && CGI.unescape(proxy.password)
|
148
251
|
]
|
149
252
|
end
|
253
|
+
|
254
|
+
# Config options for the HTTP client used for connection pooling
|
255
|
+
# @return [Hash]
|
256
|
+
def pool_config
|
257
|
+
OPTIONS.each_key.with_object({}) do |option_name, hash|
|
258
|
+
hash[option_name] = instance_variable_get("@#{option_name}")
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Helper methods extended onto Net::HTTP objects.
|
263
|
+
# @api private
|
264
|
+
class HTTP < SimpleDelegator
|
265
|
+
def initialize(http)
|
266
|
+
super(http)
|
267
|
+
@http = http
|
268
|
+
end
|
269
|
+
|
270
|
+
# @return [Integer, nil]
|
271
|
+
attr_reader :last_used
|
272
|
+
|
273
|
+
# Sends the request and tracks that this connection has been used.
|
274
|
+
def request(...)
|
275
|
+
@http.request(...)
|
276
|
+
@last_used = monotonic_milliseconds
|
277
|
+
end
|
278
|
+
|
279
|
+
def stale?
|
280
|
+
@last_used.nil? ||
|
281
|
+
(monotonic_milliseconds - @last_used) > keep_alive_timeout * 1000
|
282
|
+
end
|
283
|
+
|
284
|
+
# Attempts to close/finish the connection without raising an error.
|
285
|
+
def finish
|
286
|
+
@http.finish
|
287
|
+
rescue IOError
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
def monotonic_milliseconds
|
294
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
295
|
+
end
|
296
|
+
end
|
150
297
|
end
|
151
298
|
end
|
152
299
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Hearth
|
6
|
+
module HTTP
|
7
|
+
# An HTTP error inspector, using hints from status code and headers.
|
8
|
+
class ErrorInspector
|
9
|
+
def initialize(error, http_response)
|
10
|
+
@error = error
|
11
|
+
@http_response = http_response
|
12
|
+
end
|
13
|
+
|
14
|
+
def retryable?
|
15
|
+
(modeled_retryable? ||
|
16
|
+
throttling? ||
|
17
|
+
transient? ||
|
18
|
+
server?) &&
|
19
|
+
# IO does not respond to #truncate and is not rewindable
|
20
|
+
@http_response.body.respond_to?(:truncate)
|
21
|
+
end
|
22
|
+
|
23
|
+
def error_type
|
24
|
+
if transient?
|
25
|
+
'Transient'
|
26
|
+
elsif throttling?
|
27
|
+
'Throttling'
|
28
|
+
elsif server?
|
29
|
+
'ServerError'
|
30
|
+
elsif client?
|
31
|
+
'ClientError'
|
32
|
+
else
|
33
|
+
'Unknown'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def hints
|
38
|
+
hints = {}
|
39
|
+
if (retry_after = retry_after_hint)
|
40
|
+
hints[:retry_after] = retry_after
|
41
|
+
end
|
42
|
+
hints
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def transient?
|
48
|
+
@error.is_a?(Hearth::HTTP::NetworkingError)
|
49
|
+
end
|
50
|
+
|
51
|
+
def throttling?
|
52
|
+
@http_response.status == 429 || modeled_throttling?
|
53
|
+
end
|
54
|
+
|
55
|
+
def server?
|
56
|
+
(500..599).cover?(@http_response.status)
|
57
|
+
end
|
58
|
+
|
59
|
+
def client?
|
60
|
+
(400..499).cover?(@http_response.status)
|
61
|
+
end
|
62
|
+
|
63
|
+
def modeled_retryable?
|
64
|
+
@error.is_a?(Hearth::ApiError) && @error.retryable?
|
65
|
+
end
|
66
|
+
|
67
|
+
def modeled_throttling?
|
68
|
+
modeled_retryable? && @error.throttling?
|
69
|
+
end
|
70
|
+
|
71
|
+
def retry_after_hint
|
72
|
+
retry_after = @http_response.headers['retry-after']
|
73
|
+
Integer(retry_after)
|
74
|
+
rescue ArgumentError # string is present, assume it is a date
|
75
|
+
begin
|
76
|
+
Time.parse(retry_after) - Time.now
|
77
|
+
rescue ArgumentError # empty string, somehow
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
rescue TypeError # header is not present
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -9,17 +9,18 @@ module Hearth
|
|
9
9
|
# @api private
|
10
10
|
class ErrorParser
|
11
11
|
# @api private
|
12
|
-
HTTP_3XX = (300..399)
|
12
|
+
HTTP_3XX = (300..399)
|
13
13
|
|
14
14
|
# @api private
|
15
|
-
HTTP_4XX = (400..499)
|
15
|
+
HTTP_4XX = (400..499)
|
16
16
|
|
17
17
|
# @api private
|
18
|
-
HTTP_5XX = (500..599)
|
18
|
+
HTTP_5XX = (500..599)
|
19
19
|
|
20
20
|
# @param [Module] error_module The code generated Errors module.
|
21
21
|
# Must contain service specific implementations of
|
22
|
-
# ApiRedirectError, ApiClientError, and ApiServerError
|
22
|
+
# ApiRedirectError, ApiClientError, and ApiServerError, and it
|
23
|
+
# must have defined a `self.error_code(http_resp)` method.
|
23
24
|
#
|
24
25
|
# @param [Integer] success_status The status code of a
|
25
26
|
# successful response as defined by the model for
|
@@ -30,22 +31,21 @@ module Hearth
|
|
30
31
|
#
|
31
32
|
# @param [Array<Class<ApiError>>] errors Array of Error classes
|
32
33
|
# modeled for the operation.
|
33
|
-
|
34
|
-
# @param [callable] error_code_fn Protocol specific function
|
35
|
-
# that will return the error code from a response, or nil if
|
36
|
-
# there is none.
|
37
|
-
def initialize(error_module:, success_status:, errors:, error_code_fn:)
|
34
|
+
def initialize(error_module:, success_status:, errors:)
|
38
35
|
@error_module = error_module
|
39
36
|
@success_status = success_status
|
40
37
|
@errors = errors
|
41
|
-
@error_code_fn = error_code_fn
|
42
38
|
end
|
43
39
|
|
44
40
|
# Parse and return the error if the response is not successful.
|
45
41
|
#
|
46
|
-
# @param [Response]
|
47
|
-
|
48
|
-
|
42
|
+
# @param [HTTP::Response] http_resp The HTTP Response
|
43
|
+
# @param [Hash] metadata The metadata from {Hearth::Output}
|
44
|
+
def parse(http_resp, metadata)
|
45
|
+
error_code = @error_module.method(:error_code).call(http_resp)
|
46
|
+
return unless error?(error_code, http_resp)
|
47
|
+
|
48
|
+
create_error(error_code, http_resp, metadata)
|
49
49
|
end
|
50
50
|
|
51
51
|
private
|
@@ -59,20 +59,20 @@ module Hearth
|
|
59
59
|
# 6. Response code 5xx -> unknown server error
|
60
60
|
# [MODIFIED, 3xx, 4xx, 5xx mapped, everything else is Generic ApiError]
|
61
61
|
# 7. Everything else -> unknown client error
|
62
|
-
def error?(http_resp)
|
63
|
-
return true if
|
62
|
+
def error?(error_code, http_resp)
|
63
|
+
return true if error_code
|
64
64
|
return false if http_resp.status == @success_status
|
65
65
|
|
66
66
|
!(200..299).cover?(http_resp.status)
|
67
67
|
end
|
68
68
|
|
69
|
-
def
|
70
|
-
error_code = @error_code_fn.call(http_resp)
|
69
|
+
def create_error(error_code, http_resp, metadata)
|
71
70
|
error_class = error_class(error_code) if error_code
|
72
71
|
|
73
72
|
error_opts = {
|
74
73
|
http_resp: http_resp,
|
75
74
|
error_code: error_code,
|
75
|
+
metadata: metadata,
|
76
76
|
message: error_code # default message
|
77
77
|
}
|
78
78
|
|
@@ -84,9 +84,7 @@ module Hearth
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def error_class(error_code)
|
87
|
-
@errors.find
|
88
|
-
e.name.include? error_code
|
89
|
-
end
|
87
|
+
@errors.find { |e| e.name.include? error_code }
|
90
88
|
end
|
91
89
|
|
92
90
|
def generic_error(error_opts)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Represents an HTTP field.
|
6
|
+
class Field
|
7
|
+
# @param [String] name The name of the field.
|
8
|
+
# @param [#to_s] value (nil) The value for the field. It can be any
|
9
|
+
# object that responds to `#to_s`.
|
10
|
+
# @param [Symbol] kind The kind of field, either :header or :trailer.
|
11
|
+
def initialize(name, value = nil, kind: :header)
|
12
|
+
if name.nil? || name.empty?
|
13
|
+
raise ArgumentError, 'Field name must be a non-empty String'
|
14
|
+
end
|
15
|
+
|
16
|
+
@name = name
|
17
|
+
@value = value
|
18
|
+
@kind = kind
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
attr_reader :name
|
23
|
+
|
24
|
+
# @return [Symbol]
|
25
|
+
attr_reader :kind
|
26
|
+
|
27
|
+
# Returns a string representation of the field.
|
28
|
+
# @return [String]
|
29
|
+
def value(encoding = nil)
|
30
|
+
encoding ? @value.to_s.encode(encoding) : @value.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Boolean]
|
34
|
+
def header?
|
35
|
+
@kind == :header
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean]
|
39
|
+
def trailer?
|
40
|
+
@kind == :trailer
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash]
|
44
|
+
def to_h
|
45
|
+
{ @name => value }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Provides Hash like access for Headers and Trailers with key normalization
|
6
|
+
class Fields
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @param [Array<Field>] fields
|
10
|
+
# @param [String] encoding
|
11
|
+
def initialize(fields = [], encoding: 'utf-8')
|
12
|
+
unless fields.is_a?(Enumerable)
|
13
|
+
raise ArgumentError, 'fields must be an Enumerable of Field'
|
14
|
+
end
|
15
|
+
|
16
|
+
@entries = {}
|
17
|
+
fields.each { |field| self[field.name] = field }
|
18
|
+
@encoding = encoding
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
attr_reader :encoding
|
23
|
+
|
24
|
+
# @param [String] key
|
25
|
+
def [](key)
|
26
|
+
@entries[key.downcase]
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [String] key
|
30
|
+
# @param [Field] value
|
31
|
+
def []=(key, value)
|
32
|
+
raise ArgumentError, 'value must be a Field' unless value.is_a?(Field)
|
33
|
+
|
34
|
+
@entries[key.downcase] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [String] key
|
38
|
+
# @return [Boolean] Returns `true` if there is a Field with the given key.
|
39
|
+
def key?(key)
|
40
|
+
@entries.key?(key.downcase)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [String] key
|
44
|
+
# @return [Field, nil] Returns the Field for the deleted Field key.
|
45
|
+
def delete(key)
|
46
|
+
@entries.delete(key.downcase)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Enumerable<Field>]
|
50
|
+
def each(&block)
|
51
|
+
@entries.values.each(&block)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Integer] Returns the number of Field entries.
|
55
|
+
def size
|
56
|
+
@entries.size
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Hash]
|
60
|
+
def clear
|
61
|
+
@entries = {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def inspect
|
66
|
+
super.gsub(/ @entries={.*},/, '')
|
67
|
+
end
|
68
|
+
|
69
|
+
# Proxy class that wraps Fields to create Headers and Trailers
|
70
|
+
class Proxy
|
71
|
+
include Enumerable
|
72
|
+
|
73
|
+
def initialize(fields, kind)
|
74
|
+
@fields = fields
|
75
|
+
@kind = kind
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [String] key
|
79
|
+
def [](key)
|
80
|
+
@fields[key].value(@fields.encoding) if key?(key)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [String] key
|
84
|
+
# @param [#to_s, Array<#to_s>] value
|
85
|
+
def []=(key, value)
|
86
|
+
@fields[key] = Field.new(key, value, kind: @kind)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [String] key
|
90
|
+
# @return [Boolean] Returns `true` if there is a Field with the given
|
91
|
+
# key and kind.
|
92
|
+
def key?(key)
|
93
|
+
@fields.key?(key) && @fields[key].kind == @kind
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param [String] key
|
97
|
+
# @return [Field, nil] Returns the value for the deleted Field key.
|
98
|
+
def delete(key)
|
99
|
+
@fields.delete(key).value(@fields.encoding) if key?(key)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Enumerable<String,String>]
|
103
|
+
def each(&block)
|
104
|
+
@fields.filter { |f| f.kind == @kind }
|
105
|
+
.to_h { |f| [f.name, f.value(@fields.encoding)] }
|
106
|
+
.each(&block)
|
107
|
+
end
|
108
|
+
alias each_pair each
|
109
|
+
|
110
|
+
# @api private
|
111
|
+
def inspect
|
112
|
+
to_h
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|