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,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
|