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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -4
- data/VERSION +1 -1
- 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_stubs.rb +130 -0
- data/lib/hearth/config/env_provider.rb +53 -0
- data/lib/hearth/config/resolver.rb +52 -0
- data/lib/hearth/configuration.rb +15 -0
- data/lib/hearth/connection_pool.rb +77 -0
- data/lib/hearth/context.rb +28 -4
- data/lib/hearth/dns/host_address.rb +23 -0
- data/lib/hearth/dns/host_resolver.rb +92 -0
- data/lib/hearth/dns.rb +48 -0
- data/lib/hearth/http/api_error.rb +4 -8
- data/lib/hearth/http/client.rb +208 -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 +64 -0
- data/lib/hearth/http/fields.rb +117 -0
- data/lib/hearth/http/middleware/content_length.rb +5 -2
- data/lib/hearth/http/middleware/content_md5.rb +31 -0
- data/lib/hearth/http/middleware/request_compression.rb +157 -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 +14 -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_resolver.rb +17 -0
- data/lib/hearth/interceptor.rb +506 -0
- data/lib/hearth/interceptor_context.rb +36 -0
- data/lib/hearth/interceptor_list.rb +48 -0
- data/lib/hearth/interceptors.rb +75 -0
- data/lib/hearth/middleware/auth.rb +100 -0
- data/lib/hearth/middleware/build.rb +32 -0
- data/lib/hearth/middleware/host_prefix.rb +10 -6
- data/lib/hearth/middleware/initialize.rb +58 -0
- data/lib/hearth/middleware/parse.rb +45 -6
- data/lib/hearth/middleware/retry.rb +97 -23
- data/lib/hearth/middleware/send.rb +137 -25
- data/lib/hearth/middleware/sign.rb +65 -0
- data/lib/hearth/middleware/validate.rb +11 -1
- data/lib/hearth/middleware.rb +19 -8
- data/lib/hearth/middleware_stack.rb +1 -43
- data/lib/hearth/networking_error.rb +18 -0
- data/lib/hearth/number_helper.rb +2 -2
- data/lib/hearth/output.rb +8 -4
- data/lib/hearth/plugin_list.rb +53 -0
- data/lib/hearth/query/param.rb +52 -0
- data/lib/hearth/query/param_list.rb +54 -0
- data/lib/hearth/query/param_matcher.rb +32 -0
- data/lib/hearth/refreshing_identity_resolver.rb +63 -0
- data/lib/hearth/request.rb +22 -0
- data/lib/hearth/response.rb +33 -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 +143 -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/strategy.rb +20 -0
- data/lib/hearth/retry.rb +16 -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/stubs.rb +30 -0
- data/lib/hearth/time_helper.rb +5 -3
- data/lib/hearth/validator.rb +44 -5
- data/lib/hearth/waiters/poller.rb +6 -7
- data/lib/hearth/waiters/waiter.rb +17 -4
- data/lib/hearth/xml/formatter.rb +11 -2
- data/lib/hearth/xml/node.rb +2 -2
- data/lib/hearth.rb +32 -5
- data/sig/lib/hearth/aliases.rbs +4 -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_stubs.rbs +5 -0
- data/sig/lib/hearth/configuration.rbs +7 -0
- data/sig/lib/hearth/dns/host_address.rbs +13 -0
- data/sig/lib/hearth/dns/host_resolver.rbs +19 -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/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_resolver.rbs +7 -0
- data/sig/lib/hearth/interceptor.rbs +9 -0
- data/sig/lib/hearth/interceptor_context.rbs +15 -0
- data/sig/lib/hearth/interceptor_list.rbs +16 -0
- data/sig/lib/hearth/interfaces.rbs +65 -0
- data/sig/lib/hearth/output.rbs +11 -0
- data/sig/lib/hearth/plugin_list.rbs +15 -0
- data/sig/lib/hearth/query/param.rbs +17 -0
- data/sig/lib/hearth/query/param_list.rbs +25 -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 +7 -0
- data/sig/lib/hearth/union.rbs +5 -0
- data/sig/lib/hearth/waiters/waiter.rbs +17 -0
- metadata +132 -22
- 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
@@ -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
|
-
@
|
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 [
|
20
|
-
attr_reader :
|
18
|
+
# @return [Fields]
|
19
|
+
attr_reader :http_fields
|
21
20
|
|
22
|
-
# @return [
|
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
|
data/lib/hearth/http/client.rb
CHANGED
@@ -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
|
-
#
|
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 [
|
17
|
-
#
|
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 [
|
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
|
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 [
|
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] :
|
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
|
-
# `:
|
68
|
+
# `:ca_file` or `:ca_path` the system default
|
32
69
|
# will be used if available.
|
33
70
|
#
|
34
|
-
# @option options [String] :
|
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 `:
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
89
|
-
|
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 +=
|
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
|
-
|
192
|
+
http.ssl_timeout = @ssl_timeout
|
193
|
+
if @verify_peer
|
103
194
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
104
|
-
http.ca_file = @
|
105
|
-
http.ca_path = @
|
106
|
-
http.cert_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.
|
119
|
-
|
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
|
-
#
|
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
|
126
|
-
|
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
|
141
|
-
# @return [Array
|
142
|
-
def
|
244
|
+
# Extract the parts of the proxy URI
|
245
|
+
# @return [Array]
|
246
|
+
def proxy_parts
|
247
|
+
proxy = URI(@proxy)
|
143
248
|
[
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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)
|
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)
|