hearth 1.0.0.pre1 → 1.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/api_error.rb +15 -1
  5. data/lib/hearth/auth_option.rb +21 -0
  6. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  7. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  8. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  11. data/lib/hearth/auth_schemes.rb +32 -0
  12. data/lib/hearth/checksums.rb +31 -0
  13. data/lib/hearth/client_stubs.rb +130 -0
  14. data/lib/hearth/config/env_provider.rb +53 -0
  15. data/lib/hearth/config/resolver.rb +52 -0
  16. data/lib/hearth/configuration.rb +15 -0
  17. data/lib/hearth/connection_pool.rb +77 -0
  18. data/lib/hearth/context.rb +28 -4
  19. data/lib/hearth/dns/host_address.rb +23 -0
  20. data/lib/hearth/dns/host_resolver.rb +92 -0
  21. data/lib/hearth/dns.rb +48 -0
  22. data/lib/hearth/http/api_error.rb +4 -8
  23. data/lib/hearth/http/client.rb +208 -59
  24. data/lib/hearth/http/error_inspector.rb +85 -0
  25. data/lib/hearth/http/error_parser.rb +18 -20
  26. data/lib/hearth/http/field.rb +64 -0
  27. data/lib/hearth/http/fields.rb +117 -0
  28. data/lib/hearth/http/middleware/content_length.rb +5 -2
  29. data/lib/hearth/http/middleware/content_md5.rb +31 -0
  30. data/lib/hearth/http/middleware/request_compression.rb +157 -0
  31. data/lib/hearth/http/middleware.rb +12 -0
  32. data/lib/hearth/http/networking_error.rb +1 -14
  33. data/lib/hearth/http/request.rb +83 -56
  34. data/lib/hearth/http/response.rb +42 -13
  35. data/lib/hearth/http.rb +14 -5
  36. data/lib/hearth/identities/anonymous.rb +8 -0
  37. data/lib/hearth/identities/http_api_key.rb +16 -0
  38. data/lib/hearth/identities/http_bearer.rb +16 -0
  39. data/lib/hearth/identities/http_login.rb +20 -0
  40. data/lib/hearth/identities.rb +21 -0
  41. data/lib/hearth/identity_resolver.rb +17 -0
  42. data/lib/hearth/interceptor.rb +506 -0
  43. data/lib/hearth/interceptor_context.rb +36 -0
  44. data/lib/hearth/interceptor_list.rb +48 -0
  45. data/lib/hearth/interceptors.rb +75 -0
  46. data/lib/hearth/middleware/auth.rb +100 -0
  47. data/lib/hearth/middleware/build.rb +32 -0
  48. data/lib/hearth/middleware/host_prefix.rb +10 -6
  49. data/lib/hearth/middleware/initialize.rb +58 -0
  50. data/lib/hearth/middleware/parse.rb +45 -6
  51. data/lib/hearth/middleware/retry.rb +97 -23
  52. data/lib/hearth/middleware/send.rb +137 -25
  53. data/lib/hearth/middleware/sign.rb +65 -0
  54. data/lib/hearth/middleware/validate.rb +11 -1
  55. data/lib/hearth/middleware.rb +19 -8
  56. data/lib/hearth/middleware_stack.rb +1 -43
  57. data/lib/hearth/networking_error.rb +18 -0
  58. data/lib/hearth/number_helper.rb +2 -2
  59. data/lib/hearth/output.rb +8 -4
  60. data/lib/hearth/plugin_list.rb +53 -0
  61. data/lib/hearth/query/param.rb +52 -0
  62. data/lib/hearth/query/param_list.rb +54 -0
  63. data/lib/hearth/query/param_matcher.rb +32 -0
  64. data/lib/hearth/refreshing_identity_resolver.rb +63 -0
  65. data/lib/hearth/request.rb +22 -0
  66. data/lib/hearth/response.rb +33 -0
  67. data/lib/hearth/retry/adaptive.rb +60 -0
  68. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  69. data/lib/hearth/retry/client_rate_limiter.rb +143 -0
  70. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  71. data/lib/hearth/retry/retry_quota.rb +56 -0
  72. data/lib/hearth/retry/standard.rb +46 -0
  73. data/lib/hearth/retry/strategy.rb +20 -0
  74. data/lib/hearth/retry.rb +16 -0
  75. data/lib/hearth/signers/anonymous.rb +16 -0
  76. data/lib/hearth/signers/http_api_key.rb +29 -0
  77. data/lib/hearth/signers/http_basic.rb +23 -0
  78. data/lib/hearth/signers/http_bearer.rb +19 -0
  79. data/lib/hearth/signers/http_digest.rb +19 -0
  80. data/lib/hearth/signers.rb +23 -0
  81. data/lib/hearth/stubs.rb +30 -0
  82. data/lib/hearth/time_helper.rb +5 -3
  83. data/lib/hearth/validator.rb +44 -5
  84. data/lib/hearth/waiters/poller.rb +6 -7
  85. data/lib/hearth/waiters/waiter.rb +17 -4
  86. data/lib/hearth/xml/formatter.rb +11 -2
  87. data/lib/hearth/xml/node.rb +2 -2
  88. data/lib/hearth.rb +32 -5
  89. data/sig/lib/hearth/aliases.rbs +4 -0
  90. data/sig/lib/hearth/api_error.rbs +13 -0
  91. data/sig/lib/hearth/auth_option.rbs +11 -0
  92. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  93. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  94. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  95. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  96. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  97. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  98. data/sig/lib/hearth/block_io.rbs +9 -0
  99. data/sig/lib/hearth/client_stubs.rbs +5 -0
  100. data/sig/lib/hearth/configuration.rbs +7 -0
  101. data/sig/lib/hearth/dns/host_address.rbs +13 -0
  102. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  103. data/sig/lib/hearth/http/api_error.rbs +13 -0
  104. data/sig/lib/hearth/http/client.rbs +9 -0
  105. data/sig/lib/hearth/http/field.rbs +19 -0
  106. data/sig/lib/hearth/http/fields.rbs +43 -0
  107. data/sig/lib/hearth/http/request.rbs +25 -0
  108. data/sig/lib/hearth/http/response.rbs +21 -0
  109. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  110. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  111. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  112. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  113. data/sig/lib/hearth/identities.rbs +9 -0
  114. data/sig/lib/hearth/identity_resolver.rbs +7 -0
  115. data/sig/lib/hearth/interceptor.rbs +9 -0
  116. data/sig/lib/hearth/interceptor_context.rbs +15 -0
  117. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  118. data/sig/lib/hearth/interfaces.rbs +65 -0
  119. data/sig/lib/hearth/output.rbs +11 -0
  120. data/sig/lib/hearth/plugin_list.rbs +15 -0
  121. data/sig/lib/hearth/query/param.rbs +17 -0
  122. data/sig/lib/hearth/query/param_list.rbs +25 -0
  123. data/sig/lib/hearth/request.rbs +9 -0
  124. data/sig/lib/hearth/response.rbs +11 -0
  125. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  126. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  127. data/sig/lib/hearth/retry/standard.rbs +13 -0
  128. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  129. data/sig/lib/hearth/retry.rbs +9 -0
  130. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  131. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  132. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  133. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  134. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  135. data/sig/lib/hearth/signers.rbs +9 -0
  136. data/sig/lib/hearth/structure.rbs +7 -0
  137. data/sig/lib/hearth/union.rbs +5 -0
  138. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  139. metadata +132 -22
  140. data/lib/hearth/http/headers.rb +0 -70
  141. data/lib/hearth/middleware/around_handler.rb +0 -24
  142. data/lib/hearth/middleware/request_handler.rb +0 -24
  143. data/lib/hearth/middleware/response_handler.rb +0 -25
  144. data/lib/hearth/middleware_builder.rb +0 -246
  145. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  146. data/lib/hearth/stubbing/stubs.rb +0 -32
  147. data/lib/hearth/waiters/errors.rb +0 -15
  148. data/sig/lib/seahorse/api_error.rbs +0 -10
  149. data/sig/lib/seahorse/document.rbs +0 -2
  150. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  151. data/sig/lib/seahorse/http/headers.rbs +0 -47
  152. data/sig/lib/seahorse/http/response.rbs +0 -21
  153. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  154. data/sig/lib/seahorse/structure.rbs +0 -18
  155. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  156. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  157. data/sig/lib/seahorse/union.rbs +0 -6
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module DNS
5
+ # Resolves a host name and service to an IP address. Can be used with the
6
+ # {HTTP::Client} host_resolver option. This implementation uses
7
+ # Addrinfo.getaddrinfo to resolve the host name.
8
+ #
9
+ # @see https://ruby-doc.org/stdlib-3.0.2/libdoc/socket/rdoc/Addrinfo.html
10
+ class HostResolver
11
+ # @param [Integer] service (443)
12
+ # @param [Integer] family (nil)
13
+ # @param [Symbol] socktype (:SOCK_STREAM)
14
+ # @param [Integer] protocol (nil)
15
+ # @param [Integer] flags (nil)
16
+ def initialize(service: 443, family: nil, socktype: :SOCK_STREAM,
17
+ protocol: nil, flags: nil)
18
+ @service = service
19
+ @family = family
20
+ @socktype = socktype
21
+ @protocol = protocol
22
+ @flags = flags
23
+ end
24
+
25
+ # @return [Integer]
26
+ attr_reader :service
27
+
28
+ # @return [Integer]
29
+ attr_reader :family
30
+
31
+ # @return [Symbol]
32
+ attr_reader :socktype
33
+
34
+ # @return [Integer]
35
+ attr_reader :protocol
36
+
37
+ # @return [Integer]
38
+ attr_reader :flags
39
+
40
+ # @param [String] nodename
41
+ # @param (see Hearth::DNS::HostResolver#initialize)
42
+ def resolve_address(nodename:, **kwargs)
43
+ options = kwargs.merge(nodename: nodename)
44
+ addrinfo_list = addrinfo(options)
45
+ ipv6 = ipv6_addr(addrinfo_list, options) if use_ipv6?
46
+ ipv4 = ipv4_addr(addrinfo_list, options)
47
+ [ipv6, ipv4]
48
+ end
49
+
50
+ private
51
+
52
+ def addrinfo(options)
53
+ Addrinfo.getaddrinfo(
54
+ options[:nodename],
55
+ options.fetch(:service, @service),
56
+ options.fetch(:family, @family),
57
+ options.fetch(:socktype, @socktype),
58
+ options.fetch(:protocol, @protocol),
59
+ options.fetch(:flags, @flags)
60
+ )
61
+ end
62
+
63
+ def ipv4_addr(addrinfo_list, options)
64
+ addr = addrinfo_list.find(&:ipv4?)
65
+ return unless addr
66
+
67
+ HostAddress.new(
68
+ address_type: :A,
69
+ address: addr.ip_address,
70
+ hostname: options[:nodename]
71
+ )
72
+ end
73
+
74
+ def ipv6_addr(addrinfo_list, options)
75
+ addr = addrinfo_list.find(&:ipv6?)
76
+ return unless addr
77
+
78
+ HostAddress.new(
79
+ address_type: :AAAA,
80
+ address: addr.ip_address,
81
+ hostname: options[:nodename]
82
+ )
83
+ end
84
+
85
+ def use_ipv6?
86
+ Socket.ip_address_list.any? do |a|
87
+ a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal?
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/hearth/dns.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ require_relative 'dns/host_address'
6
+ require_relative 'dns/host_resolver'
7
+
8
+ # These patches are based on resolv-replace
9
+ # https://github.com/ruby/ruby/blob/master/lib/resolv-replace.rb
10
+ # We cannot require resolv-replace because it would change DNS resolution
11
+ # globally. When opening an HTTP request, we will set a thread local variable
12
+ # to enable custom DNS resolution, and then disable it after the request is
13
+ # complete. When the thread local variable is not set, we will use the default
14
+ # Ruby DNS resolution, which may be Resolv or the system resolver.
15
+
16
+ # Patch IPSocket
17
+ # @api private
18
+ class << IPSocket
19
+ alias original_hearth_getaddress getaddress
20
+
21
+ def getaddress(host)
22
+ unless (resolver = Thread.current[:net_http_hearth_dns_resolver])
23
+ return original_hearth_getaddress(host)
24
+ end
25
+
26
+ ipv6, ipv4 = resolver.resolve_address(nodename: host)
27
+ return ipv6.address if ipv6
28
+
29
+ ipv4.address
30
+ end
31
+ end
32
+
33
+ # Patch TCPSocket
34
+ # @api private
35
+ class TCPSocket < IPSocket
36
+ alias original_hearth_initialize initialize
37
+
38
+ # rubocop:disable Lint/MissingSuper
39
+ def initialize(host, serv, *rest)
40
+ if Thread.current[:net_http_hearth_dns_resolver]
41
+ rest[0] = IPSocket.getaddress(rest[0]) if rest[0]
42
+ original_hearth_initialize(IPSocket.getaddress(host), serv, *rest)
43
+ else
44
+ original_hearth_initialize(host, serv, *rest)
45
+ end
46
+ end
47
+ # rubocop:enable Lint/MissingSuper
48
+ end
@@ -7,23 +7,19 @@ module Hearth
7
7
  class ApiError < Hearth::ApiError
8
8
  def initialize(http_resp:, **kwargs)
9
9
  @http_status = http_resp.status
10
- @http_headers = http_resp.headers
10
+ @http_fields = http_resp.fields
11
11
  @http_body = http_resp.body
12
- @request_id = http_resp.headers['x-request-id']
13
12
  super(**kwargs)
14
13
  end
15
14
 
16
15
  # @return [Integer]
17
16
  attr_reader :http_status
18
17
 
19
- # @return [Hash<String, String>]
20
- attr_reader :http_headers
18
+ # @return [Fields]
19
+ attr_reader :http_fields
21
20
 
22
- # @return [String]
21
+ # @return [IO]
23
22
  attr_reader :http_body
24
-
25
- # @return [String]
26
- attr_reader :request_id
27
23
  end
28
24
  end
29
25
  end
@@ -1,67 +1,112 @@
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: Logger.new($stdout),
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 (Logger.new($stdout)) A logger
35
+ # used to log Net::HTTP requests and responses when `:debug_output`
36
+ # is enabled.
18
37
  #
19
- # @option options [Logger] :logger A logger where debug output is sent.
38
+ # @option options [Boolean] :debug_output (false) When `true`,
39
+ # sets an output stream to the configured Logger for debugging.
20
40
  #
21
- # @option options [URI::HTTP,String] :http_proxy A proxy to send
41
+ # @option options [String, URI] :proxy A proxy to send
22
42
  # requests through. Formatted like 'http://proxy.com:123'.
23
43
  #
24
- # @option options [Boolean] :ssl_verify_peer (true) When `true`,
44
+ # @option options [Float] :open_timeout (15) Number of seconds to
45
+ # wait for the connection to open.
46
+ #
47
+ # @option options [Float] :read_timeout Number of seconds to wait
48
+ # for one block to be read (via one read(2) call).
49
+ #
50
+ # @option options [Float] :keep_alive_timeout (5) Seconds to reuse the
51
+ # connection of the previous request.
52
+ #
53
+ # @option options [Float] :continue_timeout (1) Seconds to wait for
54
+ # 100 Continue response.
55
+ #
56
+ # @option options [Float] :write_timeout Number of seconds to wait
57
+ # for one block to be written (via one write(2) call).
58
+ #
59
+ # @option options [Float] :ssl_timeout Sets the SSL timeout seconds.
60
+ #
61
+ # @option options [Boolean] :verify_peer (true) When `true`,
25
62
  # SSL peer certificates are verified when establishing a
26
63
  # connection.
27
64
  #
28
- # @option options [String] :ssl_ca_bundle Full path to the SSL
65
+ # @option options [String] :ca_file Full path to the SSL
29
66
  # certificate authority bundle file that should be used when
30
67
  # verifying peer certificates. If you do not pass
31
- # `:ssl_ca_bundle` or `:ssl_ca_directory` the system default
68
+ # `:ca_file` or `:ca_path` the system default
32
69
  # will be used if available.
33
70
  #
34
- # @option options [String] :ssl_ca_directory Full path of the
71
+ # @option options [String] :ca_path Full path of the
35
72
  # directory that contains the unbundled SSL certificate
36
73
  # authority files for verifying peer certificates. If you do
37
- # not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the
74
+ # not pass `:ca_file` or `:ca_path` the
38
75
  # system default will be used if available.
76
+ #
77
+ # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509
78
+ # certificate store that contains the SSL certificate authority.
79
+ #
80
+ # @option options [#resolve_address] :host_resolver
81
+ # An object, such as {Hearth::DNS::HostResolver} that responds to
82
+ # `#resolve_address`, returning an array of up to two IP addresses for
83
+ # the given hostname, one IPv6 and one IPv4, in that order.
84
+ # `#resolve_address` should take a nodename keyword argument and
85
+ # optionally other keyword args similar to Addrinfo.getaddrinfo's
86
+ # positional parameters.
39
87
  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]
88
+ unknown = options.keys - OPTIONS.keys
89
+ raise ArgumentError, "Unknown options: #{unknown}" unless unknown.empty?
90
+
91
+ OPTIONS.each_pair do |opt_name, default_value|
92
+ value = options.key?(opt_name) ? options[opt_name] : default_value
93
+ instance_variable_set("@#{opt_name}", value)
94
+ end
95
+ end
96
+
97
+ OPTIONS.each_key do |attr_name|
98
+ attr_reader(attr_name)
48
99
  end
49
100
 
50
101
  # @param [Request] request
51
102
  # @param [Response] response
103
+ # @param [Logger] logger (nil)
52
104
  # @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
105
+ def transmit(request:, response:, logger: nil)
106
+ net_request = build_net_request(request)
107
+ with_connection_pool(request.uri, logger) do |connection|
108
+ _transmit(connection, net_request, response)
62
109
  end
63
-
64
- _transmit(http, request, response)
65
110
  response.body.rewind if response.body.respond_to?(:rewind)
66
111
  response
67
112
  rescue ArgumentError => e
@@ -73,37 +118,83 @@ module Hearth
73
118
 
74
119
  private
75
120
 
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
121
+ def with_connection_pool(endpoint, logger)
122
+ pool = ConnectionPool.for(pool_config)
123
+ connection = pool.connection_for(endpoint) do
124
+ new_connection(endpoint, logger)
125
+ end
126
+ yield connection
127
+ pool.offer(endpoint, connection)
128
+ rescue StandardError => e
129
+ connection&.finish
130
+ raise e
131
+ end
132
+
133
+ # Starts and returns a new HTTP connection.
134
+ # @param [URI] endpoint
135
+ # @return [Net::HTTP]
136
+ def new_connection(endpoint, logger)
137
+ http = create_http(endpoint)
138
+ http.set_debug_output(logger || @logger) if @debug_output
139
+ configure_timeouts(http)
140
+
141
+ if endpoint.scheme == 'https'
142
+ configure_ssl(http)
143
+ else
144
+ http.use_ssl = false
145
+ end
146
+
147
+ http.start
148
+ http
149
+ end
150
+
151
+ def _transmit(http, net_request, response)
152
+ # Inform monkey patch to use our DNS resolver
153
+ Thread.current[:net_http_hearth_dns_resolver] = @host_resolver
154
+ http.request(net_request) do |net_resp|
155
+ unpack_response(net_resp, response)
85
156
  end
157
+ ensure
158
+ # Restore the default DNS resolver
159
+ Thread.current[:net_http_hearth_dns_resolver] = nil
86
160
  end
87
161
 
88
- # Creates an HTTP connection to the endpoint
89
- # Applies proxy if set
162
+ def unpack_response(net_resp, response)
163
+ response.status = net_resp.code.to_i
164
+ net_resp.each_header { |k, v| response.headers[k] = v }
165
+ net_resp.read_body do |chunk|
166
+ response.body.write(chunk)
167
+ end
168
+ end
169
+
170
+ # Creates an HTTP connection to the endpoint.
171
+ # Applies proxy if set.
90
172
  def create_http(endpoint)
91
173
  args = []
92
174
  args << endpoint.host
93
175
  args << endpoint.port
94
- args += http_proxy_parts if @http_proxy
176
+ args += proxy_parts if @proxy
95
177
  # Net::HTTP.new uses positional arguments: host, port, proxy_args....
96
- Net::HTTP.new(*args.compact)
178
+ HTTP.new(Net::HTTP.new(*args.compact))
179
+ end
180
+
181
+ def configure_timeouts(http)
182
+ http.open_timeout = @open_timeout
183
+ http.keep_alive_timeout = @keep_alive_timeout
184
+ http.read_timeout = @read_timeout
185
+ http.continue_timeout = @continue_timeout
186
+ http.write_timeout = @write_timeout
97
187
  end
98
188
 
99
189
  # applies ssl settings to the HTTP object
100
190
  def configure_ssl(http)
101
191
  http.use_ssl = true
102
- if @ssl_verify_peer
192
+ http.ssl_timeout = @ssl_timeout
193
+ if @verify_peer
103
194
  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
195
+ http.ca_file = @ca_file
196
+ http.ca_path = @ca_path
197
+ http.cert_store = @cert_store
107
198
  else
108
199
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
109
200
  end
@@ -115,15 +206,28 @@ module Hearth
115
206
  # @return [Net::HTTP::Request]
116
207
  def build_net_request(request)
117
208
  request_class = net_http_request_class(request)
118
- req = request_class.new(request.url, request.headers.to_h)
119
- req.body_stream = request.body
209
+ req = request_class.new(request.uri, net_headers_for(request))
210
+
211
+ # Net::HTTP adds a default Content-Type when a body is present.
212
+ # Set the body stream when it has an unknown size or when it is > 0.
213
+ # We instead add our own Content-Length header via Middleware.
214
+ if !request.body.respond_to?(:size) ||
215
+ (request.body.respond_to?(:size) && request.body.size.positive?)
216
+ req.body_stream = request.body
217
+ end
120
218
  req
121
219
  end
122
220
 
123
- # @param [Net::HTTP::Response] response
221
+ # Validate that fields are not trailers and return a hash of headers.
222
+ # @param [HTTP::Request] request
124
223
  # @return [Hash<String, String>]
125
- def extract_headers(response)
126
- response.to_hash.transform_values(&:first)
224
+ def net_headers_for(request)
225
+ # Trailers are not supported in Net::HTTP
226
+ if request.trailers.any?
227
+ raise NotImplementedError, 'Trailers are not supported in Net::HTTP'
228
+ end
229
+
230
+ request.headers.to_h
127
231
  end
128
232
 
129
233
  # @param [Http::Request] request
@@ -137,16 +241,61 @@ module Hearth
137
241
  raise ArgumentError, msg
138
242
  end
139
243
 
140
- # Extract the parts of the http_proxy URI
141
- # @return [Array(String)]
142
- def http_proxy_parts
244
+ # Extract the parts of the proxy URI
245
+ # @return [Array]
246
+ def proxy_parts
247
+ proxy = URI(@proxy)
143
248
  [
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))
249
+ proxy.host,
250
+ proxy.port,
251
+ proxy.user && CGI.unescape(proxy.user),
252
+ proxy.password && CGI.unescape(proxy.password)
148
253
  ]
149
254
  end
255
+
256
+ # Config options for the HTTP client used for connection pooling
257
+ # @return [Hash]
258
+ def pool_config
259
+ OPTIONS.each_key.with_object({}) do |option_name, hash|
260
+ hash[option_name] = instance_variable_get("@#{option_name}")
261
+ end
262
+ end
263
+
264
+ # Helper methods extended onto Net::HTTP objects.
265
+ # @api private
266
+ class HTTP < SimpleDelegator
267
+ def initialize(http)
268
+ super(http)
269
+ @http = http
270
+ end
271
+
272
+ # @return [Integer, nil]
273
+ attr_reader :last_used
274
+
275
+ # Sends the request and tracks that this connection has been used.
276
+ def request(...)
277
+ @http.request(...)
278
+ @last_used = monotonic_milliseconds
279
+ end
280
+
281
+ def stale?
282
+ @last_used.nil? ||
283
+ (monotonic_milliseconds - @last_used) > keep_alive_timeout * 1000
284
+ end
285
+
286
+ # Attempts to close/finish the connection without raising an error.
287
+ def finish
288
+ @http.finish
289
+ rescue IOError
290
+ nil
291
+ end
292
+
293
+ private
294
+
295
+ def monotonic_milliseconds
296
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
297
+ end
298
+ end
150
299
  end
151
300
  end
152
301
  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
+ # @api private
9
+ class ErrorInspector
10
+ def initialize(error, http_response)
11
+ @error = error
12
+ @http_response = http_response
13
+ end
14
+
15
+ def retryable?
16
+ (modeled_retryable? ||
17
+ throttling? ||
18
+ transient? ||
19
+ server?) &&
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 prseent
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)