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,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module HTTP
|
|
5
|
+
# Represents an HTTP field.
|
|
6
|
+
class Field
|
|
7
|
+
# @param [String] name The name of the field.
|
|
8
|
+
# @param [Array|#to_s] value (nil) The values for the field. It can be any
|
|
9
|
+
# object that responds to `#to_s` or an Array of objects that respond to
|
|
10
|
+
# `#to_s`.
|
|
11
|
+
# @param [Symbol] kind The kind of field, either :header or :trailer.
|
|
12
|
+
def initialize(name, value = nil, kind: :header)
|
|
13
|
+
if name.nil? || name.empty?
|
|
14
|
+
raise ArgumentError, 'Field name must be a non-empty String'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@name = name
|
|
18
|
+
@value = value
|
|
19
|
+
@kind = kind
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String]
|
|
23
|
+
attr_reader :name
|
|
24
|
+
|
|
25
|
+
# @return [Symbol]
|
|
26
|
+
attr_reader :kind
|
|
27
|
+
|
|
28
|
+
# Returns an escaped string representation of the field.
|
|
29
|
+
# @return [String]
|
|
30
|
+
def value(encoding = nil)
|
|
31
|
+
value =
|
|
32
|
+
if @value.is_a?(Array)
|
|
33
|
+
@value.compact.map { |v| escape_value(v.to_s) }.join(', ')
|
|
34
|
+
else
|
|
35
|
+
@value.to_s
|
|
36
|
+
end
|
|
37
|
+
value = value.encode(encoding) if encoding
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def header?
|
|
43
|
+
@kind == :header
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def trailer?
|
|
48
|
+
@kind == :trailer
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def to_h
|
|
53
|
+
{ @name => value }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def escape_value(str)
|
|
59
|
+
s = str
|
|
60
|
+
s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module HTTP
|
|
5
|
+
# Provides Hash like access for Headers and Trailers with key normalization
|
|
6
|
+
class Fields
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
# @param [Array<Field>] fields
|
|
10
|
+
# @param [String] encoding
|
|
11
|
+
def initialize(fields = [], encoding: 'utf-8')
|
|
12
|
+
unless fields.is_a?(Enumerable)
|
|
13
|
+
raise ArgumentError, 'fields must be an Enumerable of Field'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@entries = {}
|
|
17
|
+
fields.each { |field| self[field.name] = field }
|
|
18
|
+
@encoding = encoding
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String]
|
|
22
|
+
attr_reader :encoding
|
|
23
|
+
|
|
24
|
+
# @param [String] key
|
|
25
|
+
def [](key)
|
|
26
|
+
@entries[key.downcase]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [String] key
|
|
30
|
+
# @param [Field] value
|
|
31
|
+
def []=(key, value)
|
|
32
|
+
raise ArgumentError, 'value must be a Field' unless value.is_a?(Field)
|
|
33
|
+
|
|
34
|
+
@entries[key.downcase] = value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param [String] key
|
|
38
|
+
# @return [Boolean] Returns `true` if there is a Field with the given key.
|
|
39
|
+
def key?(key)
|
|
40
|
+
@entries.key?(key.downcase)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param [String] key
|
|
44
|
+
# @return [Field, nil] Returns the Field for the deleted Field key.
|
|
45
|
+
def delete(key)
|
|
46
|
+
@entries.delete(key.downcase)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Enumerable<Field>]
|
|
50
|
+
def each(&block)
|
|
51
|
+
@entries.values.each(&block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Integer] Returns the number of Field entries.
|
|
55
|
+
def size
|
|
56
|
+
@entries.size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Hash]
|
|
60
|
+
def clear
|
|
61
|
+
@entries = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def inspect
|
|
66
|
+
super.gsub(/ @entries={.*},/, '')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Proxy class that wraps Fields to create Headers and Trailers
|
|
70
|
+
class Proxy
|
|
71
|
+
include Enumerable
|
|
72
|
+
|
|
73
|
+
def initialize(fields, kind)
|
|
74
|
+
@fields = fields
|
|
75
|
+
@kind = kind
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param [String] key
|
|
79
|
+
def [](key)
|
|
80
|
+
@fields[key].value(@fields.encoding) if key?(key)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param [String] key
|
|
84
|
+
# @param [#to_s, Array<#to_s>] value
|
|
85
|
+
def []=(key, value)
|
|
86
|
+
@fields[key] = Field.new(key, value, kind: @kind)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param [String] key
|
|
90
|
+
# @return [Boolean] Returns `true` if there is a Field with the given
|
|
91
|
+
# key and kind.
|
|
92
|
+
def key?(key)
|
|
93
|
+
@fields.key?(key) && @fields[key].kind == @kind
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param [String] key
|
|
97
|
+
# @return [Field, nil] Returns the value for the deleted Field key.
|
|
98
|
+
def delete(key)
|
|
99
|
+
@fields.delete(key).value(@fields.encoding) if key?(key)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Enumerable<String,String>]
|
|
103
|
+
def each(&block)
|
|
104
|
+
@fields.filter { |f| f.kind == @kind }
|
|
105
|
+
.to_h { |f| [f.name, f.value(@fields.encoding)] }
|
|
106
|
+
.each(&block)
|
|
107
|
+
end
|
|
108
|
+
alias each_pair each
|
|
109
|
+
|
|
110
|
+
# @api private
|
|
111
|
+
def inspect
|
|
112
|
+
to_h
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -6,6 +6,8 @@ module Hearth
|
|
|
6
6
|
# A middleware that sets Content-Length for any body that has a size.
|
|
7
7
|
# @api private
|
|
8
8
|
class ContentLength
|
|
9
|
+
include Hearth::Middleware::Logging
|
|
10
|
+
|
|
9
11
|
def initialize(app, _ = {})
|
|
10
12
|
@app = app
|
|
11
13
|
end
|
|
@@ -15,10 +17,11 @@ module Hearth
|
|
|
15
17
|
# @return [Output]
|
|
16
18
|
def call(input, context)
|
|
17
19
|
request = context.request
|
|
18
|
-
if request
|
|
19
|
-
|
|
20
|
+
if !request.headers.key?('Content-Length') &&
|
|
21
|
+
request.body.respond_to?(:size)
|
|
20
22
|
length = request.body.size
|
|
21
23
|
request.headers['Content-Length'] = length
|
|
24
|
+
log_debug(context, "Set Content-Length to #{length}")
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
@app.call(input, context)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module HTTP
|
|
5
|
+
module Middleware
|
|
6
|
+
# A middleware that sets Content-MD5 for any body.
|
|
7
|
+
# @api private
|
|
8
|
+
class ContentMD5
|
|
9
|
+
include Hearth::Middleware::Logging
|
|
10
|
+
|
|
11
|
+
def initialize(app, _ = {})
|
|
12
|
+
@app = app
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param input
|
|
16
|
+
# @param context
|
|
17
|
+
# @return [Output]
|
|
18
|
+
def call(input, context)
|
|
19
|
+
request = context.request
|
|
20
|
+
unless request.headers.key?('Content-MD5')
|
|
21
|
+
md5 = Hearth::Checksums.md5(request.body)
|
|
22
|
+
request.headers['Content-MD5'] = md5
|
|
23
|
+
log_debug(context, "Set Content-MD5 to #{md5}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@app.call(input, context)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module HTTP
|
|
5
|
+
module Middleware
|
|
6
|
+
# A middleware that compresses the request body and
|
|
7
|
+
# adds the Content-Encoding header
|
|
8
|
+
# @api private
|
|
9
|
+
class RequestCompression
|
|
10
|
+
include Hearth::Middleware::Logging
|
|
11
|
+
|
|
12
|
+
SUPPORTED_ENCODINGS = %w[gzip].freeze
|
|
13
|
+
CHUNK_SIZE = 1 * 1024 * 1024 # one MB
|
|
14
|
+
|
|
15
|
+
# @param [Class] app The next middleware in the stack.
|
|
16
|
+
# @param [Boolean] disable_request_compression If true, the request
|
|
17
|
+
# body is not compressed.
|
|
18
|
+
# @param [Integer] request_min_compression_size_bytes The minimum size
|
|
19
|
+
# of the request body to be compressed.
|
|
20
|
+
# @param [Array<String>] encodings The encodings to be used for
|
|
21
|
+
# compression.
|
|
22
|
+
# @param [Boolean] streaming If true, the request body is compressed
|
|
23
|
+
# in chunks.
|
|
24
|
+
def initialize(app, disable_request_compression:,
|
|
25
|
+
request_min_compression_size_bytes:, encodings:,
|
|
26
|
+
streaming:)
|
|
27
|
+
@app = app
|
|
28
|
+
@disable_request_compression = disable_request_compression
|
|
29
|
+
@request_min_compression_size_bytes =
|
|
30
|
+
request_min_compression_size_bytes
|
|
31
|
+
@encodings = encodings
|
|
32
|
+
@streaming = streaming
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param input
|
|
36
|
+
# @param context
|
|
37
|
+
# @return [Output]
|
|
38
|
+
def call(input, context)
|
|
39
|
+
compress_request(context) unless @disable_request_compression
|
|
40
|
+
@app.call(input, context)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def compress_request(context)
|
|
46
|
+
selected_encoding = @encodings.find do |encoding|
|
|
47
|
+
SUPPORTED_ENCODINGS.include?(encoding)
|
|
48
|
+
end
|
|
49
|
+
return unless selected_encoding
|
|
50
|
+
|
|
51
|
+
log_debug(context, "Compressing request with: #{selected_encoding}")
|
|
52
|
+
request = context.request
|
|
53
|
+
if @streaming
|
|
54
|
+
compress_streaming_body(selected_encoding, request)
|
|
55
|
+
log_debug(context, 'Compressed request body in chunks')
|
|
56
|
+
elsif request.body.size >= @request_min_compression_size_bytes
|
|
57
|
+
compress_body(selected_encoding, request)
|
|
58
|
+
log_debug(context, 'Compressed request body')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update_content_encoding(encoding, request)
|
|
63
|
+
headers = request.headers
|
|
64
|
+
if headers['Content-Encoding']
|
|
65
|
+
headers['Content-Encoding'] += ",#{encoding}"
|
|
66
|
+
else
|
|
67
|
+
headers['Content-Encoding'] = encoding
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def compress_body(encoding, request)
|
|
72
|
+
case encoding
|
|
73
|
+
when 'gzip'
|
|
74
|
+
gzip_compress(request)
|
|
75
|
+
end
|
|
76
|
+
update_content_encoding(encoding, request)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def gzip_compress(request)
|
|
80
|
+
compressed = StringIO.new
|
|
81
|
+
compressed.binmode
|
|
82
|
+
gzip_writer = Zlib::GzipWriter.new(compressed)
|
|
83
|
+
if request.body.respond_to?(:read)
|
|
84
|
+
update_in_chunks(gzip_writer, request.body)
|
|
85
|
+
else
|
|
86
|
+
gzip_writer.write(request.body)
|
|
87
|
+
end
|
|
88
|
+
gzip_writer.close
|
|
89
|
+
new_body = StringIO.new(compressed.string)
|
|
90
|
+
request.body = new_body
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def update_in_chunks(compressor, io)
|
|
94
|
+
loop do
|
|
95
|
+
chunk = io.read(CHUNK_SIZE)
|
|
96
|
+
break unless chunk
|
|
97
|
+
|
|
98
|
+
compressor.write(chunk)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def compress_streaming_body(encoding, request)
|
|
103
|
+
case encoding
|
|
104
|
+
when 'gzip'
|
|
105
|
+
request.body = GzipIO.new(request.body)
|
|
106
|
+
end
|
|
107
|
+
update_content_encoding(encoding, request)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @api private
|
|
111
|
+
class GzipIO
|
|
112
|
+
def initialize(body)
|
|
113
|
+
@body = body
|
|
114
|
+
@buffer = ChunkBuffer.new
|
|
115
|
+
@gzip_writer = Zlib::GzipWriter.new(@buffer)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read(length, buff = nil)
|
|
119
|
+
if @gzip_writer.closed?
|
|
120
|
+
# an empty string to signify an end as
|
|
121
|
+
# there will be nothing remaining to be read
|
|
122
|
+
StringIO.new('').read(length, buff)
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
chunk = @body.read(length)
|
|
127
|
+
if !chunk || chunk.empty?
|
|
128
|
+
# closing the writer will write one last chunk
|
|
129
|
+
# with a trailer (to be read from the @buffer)
|
|
130
|
+
@gzip_writer.close
|
|
131
|
+
else
|
|
132
|
+
# flush happens first to ensure that header fields
|
|
133
|
+
# are being sent over since write will override
|
|
134
|
+
@gzip_writer.flush
|
|
135
|
+
@gzip_writer.write(chunk)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
StringIO.new(@buffer.last_chunk).read(length, buff)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @api private
|
|
143
|
+
class ChunkBuffer
|
|
144
|
+
def initialize
|
|
145
|
+
@last_chunk = nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
attr_reader :last_chunk
|
|
149
|
+
|
|
150
|
+
def write(data)
|
|
151
|
+
@last_chunk = data
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'middleware/content_length'
|
|
4
|
+
require_relative 'middleware/content_md5'
|
|
5
|
+
require_relative 'middleware/request_compression'
|
|
6
|
+
|
|
7
|
+
module Hearth
|
|
8
|
+
module HTTP
|
|
9
|
+
# @api private
|
|
10
|
+
module Middleware; end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,19 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Hearth
|
|
4
4
|
module HTTP
|
|
5
|
-
|
|
6
|
-
# a request or receiving a response. You can access the original error
|
|
7
|
-
# by calling {#original_error}.
|
|
8
|
-
class NetworkingError < StandardError
|
|
9
|
-
MSG = 'Encountered an error while transmitting the request: %<message>s'
|
|
10
|
-
|
|
11
|
-
def initialize(original_error)
|
|
12
|
-
@original_error = original_error
|
|
13
|
-
super(format(MSG, message: original_error.message))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# @return [StandardError]
|
|
17
|
-
attr_reader :original_error
|
|
18
|
-
end
|
|
5
|
+
class NetworkingError < Hearth::NetworkingError; end
|
|
19
6
|
end
|
|
20
7
|
end
|
data/lib/hearth/http/request.rb
CHANGED
|
@@ -1,83 +1,77 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'stringio'
|
|
4
|
-
require 'uri'
|
|
5
|
-
|
|
6
3
|
module Hearth
|
|
7
4
|
module HTTP
|
|
8
5
|
# Represents an HTTP request.
|
|
9
|
-
|
|
10
|
-
class Request
|
|
6
|
+
class Request < Hearth::Request
|
|
11
7
|
# @param [String] http_method
|
|
12
|
-
# @param [
|
|
13
|
-
# @param
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
body: StringIO.new)
|
|
8
|
+
# @param [Fields] fields
|
|
9
|
+
# @param (see Hearth::Request#initialize)
|
|
10
|
+
def initialize(http_method: nil, fields: Fields.new, **kwargs)
|
|
11
|
+
super(**kwargs)
|
|
17
12
|
@http_method = http_method
|
|
18
|
-
@
|
|
19
|
-
@headers =
|
|
20
|
-
@
|
|
13
|
+
@fields = fields
|
|
14
|
+
@headers = Fields::Proxy.new(@fields, :header)
|
|
15
|
+
@trailers = Fields::Proxy.new(@fields, :trailer)
|
|
21
16
|
end
|
|
22
17
|
|
|
23
18
|
# @return [String]
|
|
24
19
|
attr_accessor :http_method
|
|
25
20
|
|
|
26
|
-
# @return [
|
|
27
|
-
|
|
21
|
+
# @return [Fields]
|
|
22
|
+
attr_reader :fields
|
|
28
23
|
|
|
29
|
-
# @return [
|
|
30
|
-
|
|
24
|
+
# @return [Fields::Proxy]
|
|
25
|
+
attr_reader :headers
|
|
31
26
|
|
|
32
|
-
# @return [
|
|
33
|
-
|
|
27
|
+
# @return [Fields::Proxy]
|
|
28
|
+
attr_reader :trailers
|
|
34
29
|
|
|
35
|
-
# Append a path to the HTTP request
|
|
30
|
+
# Append a path to the HTTP request URI.
|
|
36
31
|
#
|
|
37
|
-
# http_req.
|
|
32
|
+
# http_req.uri = "https://example.com"
|
|
38
33
|
# http_req.append_path('/')
|
|
39
|
-
# http_req.
|
|
34
|
+
# http_req.uri.to_s
|
|
40
35
|
# #=> "https://example.com/"
|
|
41
36
|
#
|
|
42
37
|
# Paths will be joined by a single '/':
|
|
43
38
|
#
|
|
44
|
-
# http_req.
|
|
39
|
+
# http_req.uri = "https://example.com/path-prefix/"
|
|
45
40
|
# http_req.append_path('/path-suffix')
|
|
46
|
-
# http_req.
|
|
41
|
+
# http_req.uri.to_s
|
|
47
42
|
# #=> "https://example.com/path-prefix/path-suffix"
|
|
48
43
|
#
|
|
49
|
-
# Resultant
|
|
44
|
+
# Resultant URI preserves the querystring:
|
|
50
45
|
#
|
|
51
|
-
# http_req.
|
|
46
|
+
# http_req.uri = "https://example.com/path-prefix?querystring
|
|
52
47
|
# http_req.append_path('/path-suffix')
|
|
53
|
-
# http_req.
|
|
48
|
+
# http_req.uri.to_s
|
|
54
49
|
# #=> "https://example.com/path-prefix/path-suffix?querystring"
|
|
55
50
|
#
|
|
56
51
|
# The provided path should be URI escaped before being passed.
|
|
57
52
|
#
|
|
58
|
-
# http_req.
|
|
53
|
+
# http_req.uri = "https://example.com
|
|
59
54
|
# http_req.append_path(
|
|
60
55
|
# Hearth::HTTP.uri_escape_path('/part 1/part 2')
|
|
61
56
|
# )
|
|
62
|
-
# http_req.
|
|
57
|
+
# http_req.uri.to_s
|
|
63
58
|
# #=> "https://example.com/part%201/part%202"
|
|
64
59
|
#
|
|
65
60
|
# @param [String] path A URI escaped path.
|
|
61
|
+
#
|
|
66
62
|
def append_path(path)
|
|
67
|
-
uri = URI.parse(@url)
|
|
68
63
|
base_path = uri.path.sub(%r{/$}, '') # remove trailing slash
|
|
69
64
|
path = path.sub(%r{^/}, '') # remove prefix slash
|
|
70
65
|
uri.path = "#{base_path}/#{path}" # join on single slash
|
|
71
|
-
@url = uri.to_s
|
|
72
66
|
end
|
|
73
67
|
|
|
74
|
-
# Append querystring parameter to the HTTP request
|
|
68
|
+
# Append querystring parameter to the HTTP request URI.
|
|
75
69
|
#
|
|
76
|
-
# http_req.
|
|
70
|
+
# http_req.uri = "https://example.com"
|
|
77
71
|
# http_req.append_query_param('query')
|
|
78
72
|
# http_req.append_query_param('key 1', 'value 1')
|
|
79
73
|
#
|
|
80
|
-
# http_req.
|
|
74
|
+
# http_req.uri.to_s
|
|
81
75
|
# #=> "https://example.com?query&key%201=value%201
|
|
82
76
|
#
|
|
83
77
|
# @overload append_query_param(name)
|
|
@@ -93,39 +87,72 @@ module Hearth
|
|
|
93
87
|
# The value of the querystring parameter to add. This value
|
|
94
88
|
# will be URI escaped.
|
|
95
89
|
#
|
|
96
|
-
def append_query_param(
|
|
97
|
-
param =
|
|
98
|
-
|
|
99
|
-
when 1 then escape(args[0])
|
|
100
|
-
when 2 then "#{escape(args[0])}=#{escape(args[1])}"
|
|
101
|
-
else raise ArgumentError, 'wrong number of arguments ' \
|
|
102
|
-
"(given #{args.size}, expected 1 or 2)"
|
|
103
|
-
end
|
|
104
|
-
uri = URI.parse(@url)
|
|
105
|
-
uri.query = uri.query ? "#{uri.query}&#{param}" : param
|
|
106
|
-
@url = uri.to_s
|
|
90
|
+
def append_query_param(name, value = nil)
|
|
91
|
+
param = Hearth::Query::Param.new(name, value)
|
|
92
|
+
uri.query = uri.query ? "#{uri.query}&#{param}" : param.to_s
|
|
107
93
|
end
|
|
108
94
|
|
|
109
|
-
# Append
|
|
95
|
+
# Append querystring parameter list to the HTTP request URI.
|
|
96
|
+
#
|
|
97
|
+
# http_req.uri = "https://example.com"
|
|
98
|
+
# query_params = Hearth::Query::ParamList.new
|
|
99
|
+
# query_params['key 1'] = nil
|
|
100
|
+
# query_params['key 2'] = 'value 2'
|
|
101
|
+
# http_req.append_query_param_list(query_params)
|
|
110
102
|
#
|
|
111
|
-
# http_req.
|
|
103
|
+
# http_req.uri.to_s
|
|
104
|
+
# #=> "https://example.com?key%201=&key%202=value%202"
|
|
105
|
+
#
|
|
106
|
+
# @param [ParamList] param_list
|
|
107
|
+
# An instance of Hearth::Query::ParamList containing the list of
|
|
108
|
+
# querystring parameters to add. The names and values are URI escaped.
|
|
109
|
+
#
|
|
110
|
+
def append_query_param_list(param_list)
|
|
111
|
+
return if param_list.empty?
|
|
112
|
+
|
|
113
|
+
uri.query = uri.query ? "#{uri.query}&#{param_list}" : param_list.to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Remove querystring parameter from the HTTP request URI.
|
|
117
|
+
#
|
|
118
|
+
# http_req.uri = "https://example.com"
|
|
119
|
+
# http_req.append_query_param('query')
|
|
120
|
+
# http_req.append_query_param('empty', '')
|
|
121
|
+
# http_req.append_query_param('deleteme', 'true')
|
|
122
|
+
# http_req.append_query_param('key', 'value')
|
|
123
|
+
# #=> "https://example.com?query&empty=&deleteme=true&key=value"
|
|
124
|
+
#
|
|
125
|
+
# http_req.remove_query_param('deleteme')
|
|
126
|
+
# #=> "query&empty=&key=value"
|
|
127
|
+
#
|
|
128
|
+
# http_req.uri.to_s
|
|
129
|
+
# #=> "https://example.com?query&empty=&key=value"
|
|
130
|
+
#
|
|
131
|
+
# @param [String] name The name of the querystring parameter to remove.
|
|
132
|
+
#
|
|
133
|
+
def remove_query_param(name)
|
|
134
|
+
parsed = CGI.parse(uri.query)
|
|
135
|
+
parsed.delete(name)
|
|
136
|
+
# encode_www_form ignores query params without values
|
|
137
|
+
# (CGI parses these as empty lists)
|
|
138
|
+
parsed.each do |key, values|
|
|
139
|
+
parsed[key] = values.empty? ? nil : values
|
|
140
|
+
end
|
|
141
|
+
uri.query = URI.encode_www_form(parsed)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Append a host prefix to the HTTP request URI.
|
|
145
|
+
#
|
|
146
|
+
# http_req.uri = "https://example.com"
|
|
112
147
|
# http_req.prefix_host('data.')
|
|
113
148
|
#
|
|
114
|
-
# http_req.
|
|
149
|
+
# http_req.uri.to_s
|
|
115
150
|
# #=> "https://data.foo.com
|
|
116
151
|
#
|
|
117
152
|
# @param [String] prefix A dot (.) terminated prefix for the host.
|
|
118
153
|
#
|
|
119
154
|
def prefix_host(prefix)
|
|
120
|
-
uri = URI.parse(@url)
|
|
121
155
|
uri.host = prefix + uri.host
|
|
122
|
-
@url = uri.to_s
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
private
|
|
126
|
-
|
|
127
|
-
def escape(value)
|
|
128
|
-
Hearth::HTTP.uri_escape(value.to_s)
|
|
129
156
|
end
|
|
130
157
|
end
|
|
131
158
|
end
|