hearth 1.0.0.pre1 → 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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)