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.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/anonymous_auth_resolver.rb +11 -0
  5. data/lib/hearth/api_error.rb +15 -1
  6. data/lib/hearth/auth_option.rb +21 -0
  7. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  8. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  11. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  12. data/lib/hearth/auth_schemes.rb +32 -0
  13. data/lib/hearth/checksums.rb +31 -0
  14. data/lib/hearth/client.rb +66 -0
  15. data/lib/hearth/client_stubs.rb +128 -0
  16. data/lib/hearth/config/env_provider.rb +53 -0
  17. data/lib/hearth/config/resolver.rb +53 -0
  18. data/lib/hearth/configuration.rb +15 -0
  19. data/lib/hearth/connection_pool.rb +77 -0
  20. data/lib/hearth/context.rb +29 -4
  21. data/lib/hearth/dns/host_address.rb +27 -0
  22. data/lib/hearth/dns/host_resolver.rb +92 -0
  23. data/lib/hearth/dns.rb +48 -0
  24. data/lib/hearth/endpoint_rules.rb +154 -0
  25. data/lib/hearth/http/api_error.rb +4 -8
  26. data/lib/hearth/http/client.rb +206 -59
  27. data/lib/hearth/http/error_inspector.rb +85 -0
  28. data/lib/hearth/http/error_parser.rb +18 -20
  29. data/lib/hearth/http/field.rb +49 -0
  30. data/lib/hearth/http/fields.rb +117 -0
  31. data/lib/hearth/http/header_list_builder.rb +42 -0
  32. data/lib/hearth/http/header_list_parser.rb +92 -0
  33. data/lib/hearth/http/middleware/content_length.rb +7 -4
  34. data/lib/hearth/http/middleware/content_md5.rb +30 -0
  35. data/lib/hearth/http/middleware/request_compression.rb +154 -0
  36. data/lib/hearth/http/middleware.rb +12 -0
  37. data/lib/hearth/http/networking_error.rb +1 -14
  38. data/lib/hearth/http/request.rb +83 -56
  39. data/lib/hearth/http/response.rb +42 -13
  40. data/lib/hearth/http.rb +16 -5
  41. data/lib/hearth/identities/anonymous.rb +8 -0
  42. data/lib/hearth/identities/http_api_key.rb +16 -0
  43. data/lib/hearth/identities/http_bearer.rb +16 -0
  44. data/lib/hearth/identities/http_login.rb +20 -0
  45. data/lib/hearth/identities.rb +21 -0
  46. data/lib/hearth/identity_provider.rb +17 -0
  47. data/lib/hearth/interceptor.rb +506 -0
  48. data/lib/hearth/interceptor_context.rb +40 -0
  49. data/lib/hearth/interceptor_list.rb +48 -0
  50. data/lib/hearth/interceptors.rb +76 -0
  51. data/lib/hearth/json.rb +4 -4
  52. data/lib/hearth/middleware/auth.rb +103 -0
  53. data/lib/hearth/middleware/build.rb +32 -1
  54. data/lib/hearth/middleware/endpoint.rb +79 -0
  55. data/lib/hearth/middleware/host_prefix.rb +11 -8
  56. data/lib/hearth/middleware/initialize.rb +57 -0
  57. data/lib/hearth/middleware/parse.rb +45 -7
  58. data/lib/hearth/middleware/retry.rb +105 -24
  59. data/lib/hearth/middleware/send.rb +137 -26
  60. data/lib/hearth/middleware/sign.rb +65 -0
  61. data/lib/hearth/middleware/validate.rb +11 -1
  62. data/lib/hearth/middleware.rb +20 -8
  63. data/lib/hearth/middleware_stack.rb +2 -44
  64. data/lib/hearth/networking_error.rb +18 -0
  65. data/lib/hearth/number_helper.rb +3 -3
  66. data/lib/hearth/output.rb +8 -4
  67. data/lib/hearth/plugin_list.rb +53 -0
  68. data/lib/hearth/query/param.rb +56 -0
  69. data/lib/hearth/query/param_list.rb +54 -0
  70. data/lib/hearth/query/param_matcher.rb +31 -0
  71. data/lib/hearth/refreshing_identity_provider.rb +63 -0
  72. data/lib/hearth/request.rb +22 -0
  73. data/lib/hearth/response.rb +36 -0
  74. data/lib/hearth/retry/adaptive.rb +60 -0
  75. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  76. data/lib/hearth/retry/client_rate_limiter.rb +145 -0
  77. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  78. data/lib/hearth/retry/retry_quota.rb +56 -0
  79. data/lib/hearth/retry/standard.rb +46 -0
  80. data/lib/hearth/retry.rb +29 -0
  81. data/lib/hearth/signers/anonymous.rb +16 -0
  82. data/lib/hearth/signers/http_api_key.rb +29 -0
  83. data/lib/hearth/signers/http_basic.rb +23 -0
  84. data/lib/hearth/signers/http_bearer.rb +19 -0
  85. data/lib/hearth/signers/http_digest.rb +19 -0
  86. data/lib/hearth/signers.rb +23 -0
  87. data/lib/hearth/structure.rb +7 -3
  88. data/lib/hearth/stubs.rb +38 -0
  89. data/lib/hearth/time_helper.rb +6 -5
  90. data/lib/hearth/validator.rb +60 -5
  91. data/lib/hearth/waiters/poller.rb +10 -9
  92. data/lib/hearth/waiters/waiter.rb +23 -9
  93. data/lib/hearth/xml/formatter.rb +11 -2
  94. data/lib/hearth/xml/node.rb +2 -3
  95. data/lib/hearth/xml/node_matcher.rb +0 -1
  96. data/lib/hearth.rb +37 -6
  97. data/sig/lib/hearth/aliases.rbs +6 -0
  98. data/sig/lib/hearth/anonymous_auth_resolver.rbs +5 -0
  99. data/sig/lib/hearth/api_error.rbs +13 -0
  100. data/sig/lib/hearth/auth_option.rbs +11 -0
  101. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  102. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  103. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  104. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  105. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  106. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  107. data/sig/lib/hearth/block_io.rbs +9 -0
  108. data/sig/lib/hearth/client.rbs +9 -0
  109. data/sig/lib/hearth/client_stubs.rbs +5 -0
  110. data/sig/lib/hearth/configuration.rbs +7 -0
  111. data/sig/lib/hearth/dns/host_address.rbs +11 -0
  112. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  113. data/sig/lib/hearth/endpoint_rules.rbs +17 -0
  114. data/sig/lib/hearth/http/api_error.rbs +13 -0
  115. data/sig/lib/hearth/http/client.rbs +9 -0
  116. data/sig/lib/hearth/http/field.rbs +19 -0
  117. data/sig/lib/hearth/http/fields.rbs +43 -0
  118. data/sig/lib/hearth/http/header_list_builder.rbs +15 -0
  119. data/sig/lib/hearth/http/header_list_parser.rbs +19 -0
  120. data/sig/lib/hearth/http/networking_error.rbs +6 -0
  121. data/sig/lib/hearth/http/request.rbs +25 -0
  122. data/sig/lib/hearth/http/response.rbs +21 -0
  123. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  124. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  125. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  126. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  127. data/sig/lib/hearth/identities.rbs +9 -0
  128. data/sig/lib/hearth/identity_provider.rbs +7 -0
  129. data/sig/lib/hearth/interceptor.rbs +9 -0
  130. data/sig/lib/hearth/interceptor_context.rbs +17 -0
  131. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  132. data/sig/lib/hearth/interfaces.rbs +87 -0
  133. data/sig/lib/hearth/json/parse_error.rbs +9 -0
  134. data/sig/lib/hearth/networking_error.rbs +7 -0
  135. data/sig/lib/hearth/output.rbs +11 -0
  136. data/sig/lib/hearth/plugin_list.rbs +13 -0
  137. data/sig/lib/hearth/query/param.rbs +17 -0
  138. data/sig/lib/hearth/query/param_list.rbs +25 -0
  139. data/sig/lib/hearth/refreshing_identity_provider.rbs +10 -0
  140. data/sig/lib/hearth/request.rbs +9 -0
  141. data/sig/lib/hearth/response.rbs +11 -0
  142. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  143. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  144. data/sig/lib/hearth/retry/standard.rbs +13 -0
  145. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  146. data/sig/lib/hearth/retry.rbs +9 -0
  147. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  148. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  149. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  150. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  151. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  152. data/sig/lib/hearth/signers.rbs +9 -0
  153. data/sig/lib/hearth/structure.rbs +6 -0
  154. data/sig/lib/hearth/stubs.rbs +9 -0
  155. data/sig/lib/hearth/union.rbs +5 -0
  156. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  157. data/sig/lib/hearth/xml/parse_error.rbs +9 -0
  158. metadata +151 -25
  159. data/lib/hearth/http/headers.rb +0 -70
  160. data/lib/hearth/middleware/around_handler.rb +0 -24
  161. data/lib/hearth/middleware/request_handler.rb +0 -24
  162. data/lib/hearth/middleware/response_handler.rb +0 -25
  163. data/lib/hearth/middleware_builder.rb +0 -246
  164. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  165. data/lib/hearth/stubbing/stubs.rb +0 -32
  166. data/lib/hearth/waiters/errors.rb +0 -15
  167. data/sig/lib/seahorse/api_error.rbs +0 -10
  168. data/sig/lib/seahorse/document.rbs +0 -2
  169. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  170. data/sig/lib/seahorse/http/headers.rbs +0 -47
  171. data/sig/lib/seahorse/http/response.rbs +0 -21
  172. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  173. data/sig/lib/seahorse/structure.rbs +0 -18
  174. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  175. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  176. data/sig/lib/seahorse/union.rbs +0 -6
@@ -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
- # Transmits an HTTP {Request} object, returning an HTTP {Response}.
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 [Boolean] :http_wire_trace (false) When `true`,
17
- # HTTP debug output will be sent to the `:logger`.
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 [Logger] :logger A logger where debug output is sent.
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::HTTP,String] :http_proxy A proxy to send
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 [Boolean] :ssl_verify_peer (true) When `true`,
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] :ssl_ca_bundle Full path to the SSL
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
- # `:ssl_ca_bundle` or `:ssl_ca_directory` the system default
67
+ # `:ca_file` or `:ca_path` the system default
32
68
  # will be used if available.
33
69
  #
34
- # @option options [String] :ssl_ca_directory Full path of the
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 `:ssl_ca_bundle` or `:ssl_ca_directory` the
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
- @http_wire_trace = options[:http_wire_trace]
41
- @logger = options[:logger]
42
- @http_proxy = options[:http_proxy]
43
- @http_proxy = URI.parse(@http_proxy.to_s) if @http_proxy
44
- @ssl_verify_peer = options[:ssl_verify_peer]
45
- @ssl_ca_bundle = options[:ssl_ca_bundle]
46
- @ssl_ca_directory = options[:ssl_ca_directory]
47
- @ssl_ca_store = options[:ssl_ca_store]
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
- uri = URI.parse(request.url)
55
- http = create_http(uri)
56
- http.set_debug_output(@logger) if @http_wire_trace
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 _transmit(http, request, response)
77
- http.start do |conn|
78
- conn.request(build_net_request(request)) do |net_resp|
79
- response.status = net_resp.code.to_i
80
- response.headers = extract_headers(net_resp)
81
- net_resp.read_body do |chunk|
82
- response.body.write(chunk)
83
- end
84
- end
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
- # Creates an HTTP connection to the endpoint
89
- # Applies proxy if set
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 += http_proxy_parts if @http_proxy
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
- if @ssl_verify_peer
190
+ http.ssl_timeout = @ssl_timeout
191
+ if @verify_peer
103
192
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
104
- http.ca_file = @ssl_ca_bundle if @ssl_ca_bundle
105
- http.ca_path = @ssl_ca_directory if @ssl_ca_directory
106
- http.cert_store = @ssl_ca_store if @ssl_ca_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.url, request.headers.to_h)
119
- req.body_stream = request.body
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
- # @param [Net::HTTP::Response] response
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 extract_headers(response)
126
- response.to_hash.transform_values(&:first)
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 http_proxy URI
141
- # @return [Array(String)]
142
- def http_proxy_parts
242
+ # Extract the parts of the proxy URI
243
+ # @return [Array]
244
+ def proxy_parts
245
+ proxy = URI(@proxy)
143
246
  [
144
- @http_proxy.host,
145
- @http_proxy.port,
146
- (@http_proxy.user && CGI.unescape(@http_proxy.user)),
147
- (@http_proxy.password && CGI.unescape(@http_proxy.password))
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).freeze
12
+ HTTP_3XX = (300..399)
13
13
 
14
14
  # @api private
15
- HTTP_4XX = (400..499).freeze
15
+ HTTP_4XX = (400..499)
16
16
 
17
17
  # @api private
18
- HTTP_5XX = (500..599).freeze
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] response The HTTP response
47
- def parse(response)
48
- extract_error(response) if error?(response)
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 @error_code_fn.call(http_resp)
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 extract_error(http_resp)
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 do |e|
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