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.
- 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)
|