hearth 1.0.0.pre1
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 +7 -0
- data/CHANGELOG.md +12 -0
- data/VERSION +1 -0
- data/lib/hearth/api_error.rb +15 -0
- data/lib/hearth/block_io.rb +24 -0
- data/lib/hearth/context.rb +34 -0
- data/lib/hearth/http/api_error.rb +29 -0
- data/lib/hearth/http/client.rb +152 -0
- data/lib/hearth/http/error_parser.rb +105 -0
- data/lib/hearth/http/headers.rb +70 -0
- data/lib/hearth/http/middleware/content_length.rb +29 -0
- data/lib/hearth/http/networking_error.rb +20 -0
- data/lib/hearth/http/request.rb +132 -0
- data/lib/hearth/http/response.rb +29 -0
- data/lib/hearth/http.rb +36 -0
- data/lib/hearth/json/parse_error.rb +18 -0
- data/lib/hearth/json.rb +30 -0
- data/lib/hearth/middleware/around_handler.rb +24 -0
- data/lib/hearth/middleware/build.rb +26 -0
- data/lib/hearth/middleware/host_prefix.rb +48 -0
- data/lib/hearth/middleware/parse.rb +42 -0
- data/lib/hearth/middleware/request_handler.rb +24 -0
- data/lib/hearth/middleware/response_handler.rb +25 -0
- data/lib/hearth/middleware/retry.rb +43 -0
- data/lib/hearth/middleware/send.rb +62 -0
- data/lib/hearth/middleware/validate.rb +29 -0
- data/lib/hearth/middleware.rb +16 -0
- data/lib/hearth/middleware_builder.rb +246 -0
- data/lib/hearth/middleware_stack.rb +73 -0
- data/lib/hearth/number_helper.rb +33 -0
- data/lib/hearth/output.rb +20 -0
- data/lib/hearth/structure.rb +40 -0
- data/lib/hearth/stubbing/client_stubs.rb +115 -0
- data/lib/hearth/stubbing/stubs.rb +32 -0
- data/lib/hearth/time_helper.rb +35 -0
- data/lib/hearth/union.rb +10 -0
- data/lib/hearth/validator.rb +20 -0
- data/lib/hearth/waiters/errors.rb +15 -0
- data/lib/hearth/waiters/poller.rb +132 -0
- data/lib/hearth/waiters/waiter.rb +79 -0
- data/lib/hearth/xml/formatter.rb +68 -0
- data/lib/hearth/xml/node.rb +123 -0
- data/lib/hearth/xml/node_matcher.rb +24 -0
- data/lib/hearth/xml/parse_error.rb +18 -0
- data/lib/hearth/xml.rb +58 -0
- data/lib/hearth.rb +26 -0
- data/sig/lib/seahorse/api_error.rbs +10 -0
- data/sig/lib/seahorse/document.rbs +2 -0
- data/sig/lib/seahorse/http/api_error.rbs +21 -0
- data/sig/lib/seahorse/http/headers.rbs +47 -0
- data/sig/lib/seahorse/http/response.rbs +21 -0
- data/sig/lib/seahorse/simple_delegator.rbs +3 -0
- data/sig/lib/seahorse/structure.rbs +18 -0
- data/sig/lib/seahorse/stubbing/client_stubs.rbs +103 -0
- data/sig/lib/seahorse/stubbing/stubs.rbs +14 -0
- data/sig/lib/seahorse/union.rbs +6 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b43b47b8abef04f52e1f151032c376b90b3112e23c3084fbba6dd777e3f95180
|
4
|
+
data.tar.gz: a902f668849d1c28d0afd66ce2c3b80eac6a5626187e3c44a1d13d99234576e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4958477705f77ead625d1bbb49b2c11b0e44542af63bf9aaa95d87bba82a73cdbed65b40461f56e6bc9388cb612cf4cb4c341e7ba1a9b2502076ffa809e3cf14
|
7
|
+
data.tar.gz: 44f0d91c07b0f431b9660f459307daebbbbe8824ad90b4009cfdff63fa139c340201ed88667f80909bbd973bdc40216c74efd4601ce9fbbfb5d3c81e2f2bed2d
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Unreleased Changes
|
2
|
+
------------------
|
3
|
+
|
4
|
+
1.0.0.pre1 (2022-01-10)
|
5
|
+
------------------
|
6
|
+
|
7
|
+
* Feature - Initial public pre-release for Smithy Ruby SDKs.
|
8
|
+
|
9
|
+
0.1.0 (2013-29-04)
|
10
|
+
------------------
|
11
|
+
|
12
|
+
Not intended for public usage. Used as an internal detail of AWS SDK For Ruby v2 and v3.
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0.pre1
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
# Base class for errors returned from an API. This excludes networking
|
5
|
+
# errors and errors generated on the client-side.
|
6
|
+
class ApiError < StandardError
|
7
|
+
def initialize(error_code:, message: nil)
|
8
|
+
@error_code = error_code
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :error_code
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
# Given a block, BlockIO will call it with data to write and return the
|
5
|
+
# bytesize written. BlockIO keeps track of all bytes yielded.
|
6
|
+
class BlockIO
|
7
|
+
# @param [Proc] block
|
8
|
+
def initialize(block)
|
9
|
+
@block = block
|
10
|
+
@bytes_yielded = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Integer]
|
14
|
+
attr_reader :bytes_yielded
|
15
|
+
|
16
|
+
# @param [String] data
|
17
|
+
# @return [Integer] Returns the number of bytes written.
|
18
|
+
def write(data)
|
19
|
+
@block.call(data)
|
20
|
+
@bytes_yielded += data.bytesize
|
21
|
+
data.bytesize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
# Stores request and response objects, and other useful things used by
|
5
|
+
# multiple Middleware.
|
6
|
+
class Context
|
7
|
+
def initialize(options = {})
|
8
|
+
@operation_name = options[:operation_name]
|
9
|
+
@request = options[:request]
|
10
|
+
@response = options[:response]
|
11
|
+
@logger = options[:logger]
|
12
|
+
@params = options[:params]
|
13
|
+
@metadata = options[:metadata] || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Symbol] Name of the API operation called.
|
17
|
+
attr_reader :operation_name
|
18
|
+
|
19
|
+
# @return [Hearth::HTTP::Request]
|
20
|
+
attr_reader :request
|
21
|
+
|
22
|
+
# @return [Hearth::HTTP::Response]
|
23
|
+
attr_reader :response
|
24
|
+
|
25
|
+
# @return [Logger] An instance of the logger configured for the Client.
|
26
|
+
attr_reader :logger
|
27
|
+
|
28
|
+
# @return [Hash] The hash of the original request parameters.
|
29
|
+
attr_reader :params
|
30
|
+
|
31
|
+
# @return [Hash]
|
32
|
+
attr_reader :metadata
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Base class for HTTP errors returned from an API. Inherits from
|
6
|
+
# {Hearth::ApiError}.
|
7
|
+
class ApiError < Hearth::ApiError
|
8
|
+
def initialize(http_resp:, **kwargs)
|
9
|
+
@http_status = http_resp.status
|
10
|
+
@http_headers = http_resp.headers
|
11
|
+
@http_body = http_resp.body
|
12
|
+
@request_id = http_resp.headers['x-request-id']
|
13
|
+
super(**kwargs)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Integer]
|
17
|
+
attr_reader :http_status
|
18
|
+
|
19
|
+
# @return [Hash<String, String>]
|
20
|
+
attr_reader :http_headers
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :http_body
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :request_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'logger'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
module Hearth
|
8
|
+
module HTTP
|
9
|
+
# Transmits an HTTP {Request} object, returning an HTTP {Response}.
|
10
|
+
# @api private
|
11
|
+
class Client
|
12
|
+
# Initialize an instance of this HTTP client.
|
13
|
+
#
|
14
|
+
# @param [Hash] options The options for this HTTP Client
|
15
|
+
#
|
16
|
+
# @option options [Boolean] :http_wire_trace (false) When `true`,
|
17
|
+
# HTTP debug output will be sent to the `:logger`.
|
18
|
+
#
|
19
|
+
# @option options [Logger] :logger A logger where debug output is sent.
|
20
|
+
#
|
21
|
+
# @option options [URI::HTTP,String] :http_proxy A proxy to send
|
22
|
+
# requests through. Formatted like 'http://proxy.com:123'.
|
23
|
+
#
|
24
|
+
# @option options [Boolean] :ssl_verify_peer (true) When `true`,
|
25
|
+
# SSL peer certificates are verified when establishing a
|
26
|
+
# connection.
|
27
|
+
#
|
28
|
+
# @option options [String] :ssl_ca_bundle Full path to the SSL
|
29
|
+
# certificate authority bundle file that should be used when
|
30
|
+
# verifying peer certificates. If you do not pass
|
31
|
+
# `:ssl_ca_bundle` or `:ssl_ca_directory` the system default
|
32
|
+
# will be used if available.
|
33
|
+
#
|
34
|
+
# @option options [String] :ssl_ca_directory Full path of the
|
35
|
+
# directory that contains the unbundled SSL certificate
|
36
|
+
# authority files for verifying peer certificates. If you do
|
37
|
+
# not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the
|
38
|
+
# system default will be used if available.
|
39
|
+
def initialize(options = {})
|
40
|
+
@http_wire_trace = options[:http_wire_trace]
|
41
|
+
@logger = options[:logger]
|
42
|
+
@http_proxy = options[:http_proxy]
|
43
|
+
@http_proxy = URI.parse(@http_proxy.to_s) if @http_proxy
|
44
|
+
@ssl_verify_peer = options[:ssl_verify_peer]
|
45
|
+
@ssl_ca_bundle = options[:ssl_ca_bundle]
|
46
|
+
@ssl_ca_directory = options[:ssl_ca_directory]
|
47
|
+
@ssl_ca_store = options[:ssl_ca_store]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param [Request] request
|
51
|
+
# @param [Response] response
|
52
|
+
# @return [Response]
|
53
|
+
def transmit(request:, response:)
|
54
|
+
uri = URI.parse(request.url)
|
55
|
+
http = create_http(uri)
|
56
|
+
http.set_debug_output(@logger) if @http_wire_trace
|
57
|
+
|
58
|
+
if uri.scheme == 'https'
|
59
|
+
configure_ssl(http)
|
60
|
+
else
|
61
|
+
http.use_ssl = false
|
62
|
+
end
|
63
|
+
|
64
|
+
_transmit(http, request, response)
|
65
|
+
response.body.rewind if response.body.respond_to?(:rewind)
|
66
|
+
response
|
67
|
+
rescue ArgumentError => e
|
68
|
+
# Invalid verb, ArgumentError is a StandardError
|
69
|
+
raise e
|
70
|
+
rescue StandardError => e
|
71
|
+
raise Hearth::HTTP::NetworkingError, e
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def _transmit(http, request, response)
|
77
|
+
http.start do |conn|
|
78
|
+
conn.request(build_net_request(request)) do |net_resp|
|
79
|
+
response.status = net_resp.code.to_i
|
80
|
+
response.headers = extract_headers(net_resp)
|
81
|
+
net_resp.read_body do |chunk|
|
82
|
+
response.body.write(chunk)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates an HTTP connection to the endpoint
|
89
|
+
# Applies proxy if set
|
90
|
+
def create_http(endpoint)
|
91
|
+
args = []
|
92
|
+
args << endpoint.host
|
93
|
+
args << endpoint.port
|
94
|
+
args += http_proxy_parts if @http_proxy
|
95
|
+
# Net::HTTP.new uses positional arguments: host, port, proxy_args....
|
96
|
+
Net::HTTP.new(*args.compact)
|
97
|
+
end
|
98
|
+
|
99
|
+
# applies ssl settings to the HTTP object
|
100
|
+
def configure_ssl(http)
|
101
|
+
http.use_ssl = true
|
102
|
+
if @ssl_verify_peer
|
103
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
104
|
+
http.ca_file = @ssl_ca_bundle if @ssl_ca_bundle
|
105
|
+
http.ca_path = @ssl_ca_directory if @ssl_ca_directory
|
106
|
+
http.cert_store = @ssl_ca_store if @ssl_ca_store
|
107
|
+
else
|
108
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Constructs and returns a Net::HTTP::Request object from
|
113
|
+
# a {Http::Request}.
|
114
|
+
# @param [Http::Request] request
|
115
|
+
# @return [Net::HTTP::Request]
|
116
|
+
def build_net_request(request)
|
117
|
+
request_class = net_http_request_class(request)
|
118
|
+
req = request_class.new(request.url, request.headers.to_h)
|
119
|
+
req.body_stream = request.body
|
120
|
+
req
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param [Net::HTTP::Response] response
|
124
|
+
# @return [Hash<String, String>]
|
125
|
+
def extract_headers(response)
|
126
|
+
response.to_hash.transform_values(&:first)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @param [Http::Request] request
|
130
|
+
# @raise [InvalidHttpVerbError]
|
131
|
+
# @return Returns a base `Net::HTTP::Request` class, e.g.,
|
132
|
+
# `Net::HTTP::Get`, `Net::HTTP::Post`, etc.
|
133
|
+
def net_http_request_class(request)
|
134
|
+
Net::HTTP.const_get(request.http_method.capitalize)
|
135
|
+
rescue NameError
|
136
|
+
msg = "`#{request.http_method}` is not a valid http verb"
|
137
|
+
raise ArgumentError, msg
|
138
|
+
end
|
139
|
+
|
140
|
+
# Extract the parts of the http_proxy URI
|
141
|
+
# @return [Array(String)]
|
142
|
+
def http_proxy_parts
|
143
|
+
[
|
144
|
+
@http_proxy.host,
|
145
|
+
@http_proxy.port,
|
146
|
+
(@http_proxy.user && CGI.unescape(@http_proxy.user)),
|
147
|
+
(@http_proxy.password && CGI.unescape(@http_proxy.password))
|
148
|
+
]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Uses HTTP specific logic + Protocol defined Errors and
|
6
|
+
# Error Code function to determine if a response should
|
7
|
+
# be parsed as an Error. Contains generic Error parsing
|
8
|
+
# logic as well.
|
9
|
+
# @api private
|
10
|
+
class ErrorParser
|
11
|
+
# @api private
|
12
|
+
HTTP_3XX = (300..399).freeze
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
HTTP_4XX = (400..499).freeze
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
HTTP_5XX = (500..599).freeze
|
19
|
+
|
20
|
+
# @param [Module] error_module The code generated Errors module.
|
21
|
+
# Must contain service specific implementations of
|
22
|
+
# ApiRedirectError, ApiClientError, and ApiServerError
|
23
|
+
#
|
24
|
+
# @param [Integer] success_status The status code of a
|
25
|
+
# successful response as defined by the model for
|
26
|
+
# this operation. If this is a non 2XX value,
|
27
|
+
# the request will be considered successful if
|
28
|
+
# it has the success_status and does not
|
29
|
+
# have an error code.
|
30
|
+
#
|
31
|
+
# @param [Array<Class<ApiError>>] errors Array of Error classes
|
32
|
+
# 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:)
|
38
|
+
@error_module = error_module
|
39
|
+
@success_status = success_status
|
40
|
+
@errors = errors
|
41
|
+
@error_code_fn = error_code_fn
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parse and return the error if the response is not successful.
|
45
|
+
#
|
46
|
+
# @param [Response] response The HTTP response
|
47
|
+
def parse(response)
|
48
|
+
extract_error(response) if error?(response)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Implements the following order of precedence
|
54
|
+
# 1. Response has error_code -> error
|
55
|
+
# 2. Response code == http trait status code? -> success
|
56
|
+
# 3. Response code matches any error status codes? -> error
|
57
|
+
# [EXCLUDED, covered by error_code]
|
58
|
+
# 4. Response code is 2xx? -> success
|
59
|
+
# 6. Response code 5xx -> unknown server error
|
60
|
+
# [MODIFIED, 3xx, 4xx, 5xx mapped, everything else is Generic ApiError]
|
61
|
+
# 7. Everything else -> unknown client error
|
62
|
+
def error?(http_resp)
|
63
|
+
return true if @error_code_fn.call(http_resp)
|
64
|
+
return false if http_resp.status == @success_status
|
65
|
+
|
66
|
+
!(200..299).cover?(http_resp.status)
|
67
|
+
end
|
68
|
+
|
69
|
+
def extract_error(http_resp)
|
70
|
+
error_code = @error_code_fn.call(http_resp)
|
71
|
+
error_class = error_class(error_code) if error_code
|
72
|
+
|
73
|
+
error_opts = {
|
74
|
+
http_resp: http_resp,
|
75
|
+
error_code: error_code,
|
76
|
+
message: error_code # default message
|
77
|
+
}
|
78
|
+
|
79
|
+
if error_class
|
80
|
+
error_class.new(**error_opts)
|
81
|
+
else
|
82
|
+
generic_error(error_opts)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def error_class(error_code)
|
87
|
+
@errors.find do |e|
|
88
|
+
e.name.include? error_code
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def generic_error(error_opts)
|
93
|
+
http_resp = error_opts[:http_resp]
|
94
|
+
case http_resp.status
|
95
|
+
when HTTP_3XX then @error_module::ApiRedirectError.new(
|
96
|
+
location: http_resp.headers['location'], **error_opts
|
97
|
+
)
|
98
|
+
when HTTP_4XX then @error_module::ApiClientError.new(**error_opts)
|
99
|
+
when HTTP_5XX then @error_module::ApiServerError.new(**error_opts)
|
100
|
+
else @error_module::ApiError.new(**error_opts)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Provides Hash like access for Headers with key normalization
|
6
|
+
# @api private
|
7
|
+
class Headers
|
8
|
+
# @param [Hash<String,String>] headers
|
9
|
+
def initialize(headers: {})
|
10
|
+
@headers = {}
|
11
|
+
headers.each_pair do |key, value|
|
12
|
+
self[key] = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [String] key
|
17
|
+
def [](key)
|
18
|
+
@headers[normalize(key)]
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [String] key
|
22
|
+
# @param [String] value
|
23
|
+
def []=(key, value)
|
24
|
+
@headers[normalize(key)] = value.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [String] key
|
28
|
+
# @return [Boolean] Returns `true` if there is a header with
|
29
|
+
# the given key.
|
30
|
+
def key?(key)
|
31
|
+
@headers.key?(normalize(key))
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Array<String>]
|
35
|
+
def keys
|
36
|
+
@headers.keys
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [String] key
|
40
|
+
# @return [String, nil] Returns the value for the deleted key.
|
41
|
+
def delete(key)
|
42
|
+
@headers.delete(normalize(key))
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Enumerable<String,String>]
|
46
|
+
def each_pair(&block)
|
47
|
+
@headers.each(&block)
|
48
|
+
end
|
49
|
+
alias each each_pair
|
50
|
+
|
51
|
+
# @return [Hash]
|
52
|
+
def to_hash
|
53
|
+
@headers.dup
|
54
|
+
end
|
55
|
+
alias to_h to_hash
|
56
|
+
|
57
|
+
# @return [Integer] Returns the number of entries in the headers
|
58
|
+
# hash.
|
59
|
+
def size
|
60
|
+
@headers.size
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def normalize(key)
|
66
|
+
key.to_s.gsub(/[^-]+/, &:capitalize)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
module Middleware
|
6
|
+
# A middleware that sets Content-Length for any body that has a size.
|
7
|
+
# @api private
|
8
|
+
class ContentLength
|
9
|
+
def initialize(app, _ = {})
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param input
|
14
|
+
# @param context
|
15
|
+
# @return [Output]
|
16
|
+
def call(input, context)
|
17
|
+
request = context.request
|
18
|
+
if request&.body.respond_to?(:size) &&
|
19
|
+
!request.headers.key?('Content-Length')
|
20
|
+
length = request.body.size
|
21
|
+
request.headers['Content-Length'] = length
|
22
|
+
end
|
23
|
+
|
24
|
+
@app.call(input, context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hearth
|
4
|
+
module HTTP
|
5
|
+
# Thrown by a Client when encountering a networking error while transmitting
|
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
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Hearth
|
7
|
+
module HTTP
|
8
|
+
# Represents an HTTP request.
|
9
|
+
# @api private
|
10
|
+
class Request
|
11
|
+
# @param [String] http_method
|
12
|
+
# @param [String] url
|
13
|
+
# @param [Headers] headers
|
14
|
+
# @param [IO] body
|
15
|
+
def initialize(http_method: nil, url: nil, headers: Headers.new,
|
16
|
+
body: StringIO.new)
|
17
|
+
@http_method = http_method
|
18
|
+
@url = url
|
19
|
+
@headers = headers
|
20
|
+
@body = body
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [String]
|
24
|
+
attr_accessor :http_method
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
attr_accessor :url
|
28
|
+
|
29
|
+
# @return [Headers]
|
30
|
+
attr_accessor :headers
|
31
|
+
|
32
|
+
# @return [IO]
|
33
|
+
attr_accessor :body
|
34
|
+
|
35
|
+
# Append a path to the HTTP request URL.
|
36
|
+
#
|
37
|
+
# http_req.url = "https://example.com"
|
38
|
+
# http_req.append_path('/')
|
39
|
+
# http_req.url
|
40
|
+
# #=> "https://example.com/"
|
41
|
+
#
|
42
|
+
# Paths will be joined by a single '/':
|
43
|
+
#
|
44
|
+
# http_req.url = "https://example.com/path-prefix/"
|
45
|
+
# http_req.append_path('/path-suffix')
|
46
|
+
# http_req.url
|
47
|
+
# #=> "https://example.com/path-prefix/path-suffix"
|
48
|
+
#
|
49
|
+
# Resultant URL preserves the querystring:
|
50
|
+
#
|
51
|
+
# http_req.url = "https://example.com/path-prefix?querystring
|
52
|
+
# http_req.append_path('/path-suffix')
|
53
|
+
# http_req.url
|
54
|
+
# #=> "https://example.com/path-prefix/path-suffix?querystring"
|
55
|
+
#
|
56
|
+
# The provided path should be URI escaped before being passed.
|
57
|
+
#
|
58
|
+
# http_req.url = "https://example.com
|
59
|
+
# http_req.append_path(
|
60
|
+
# Hearth::HTTP.uri_escape_path('/part 1/part 2')
|
61
|
+
# )
|
62
|
+
# http_req.url
|
63
|
+
# #=> "https://example.com/part%201/part%202"
|
64
|
+
#
|
65
|
+
# @param [String] path A URI escaped path.
|
66
|
+
def append_path(path)
|
67
|
+
uri = URI.parse(@url)
|
68
|
+
base_path = uri.path.sub(%r{/$}, '') # remove trailing slash
|
69
|
+
path = path.sub(%r{^/}, '') # remove prefix slash
|
70
|
+
uri.path = "#{base_path}/#{path}" # join on single slash
|
71
|
+
@url = uri.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
# Append querystring parameter to the HTTP request URL.
|
75
|
+
#
|
76
|
+
# http_req.url = "https://example.com"
|
77
|
+
# http_req.append_query_param('query')
|
78
|
+
# http_req.append_query_param('key 1', 'value 1')
|
79
|
+
#
|
80
|
+
# http_req.url
|
81
|
+
# #=> "https://example.com?query&key%201=value%201
|
82
|
+
#
|
83
|
+
# @overload append_query_param(name)
|
84
|
+
# @param [String] name
|
85
|
+
# The name of the querystring parameter to add. This name
|
86
|
+
# will be URI escaped.
|
87
|
+
#
|
88
|
+
# @overload append_query_param(name, value)
|
89
|
+
# @param [String] name
|
90
|
+
# The name of the querystring parameter to add. This name
|
91
|
+
# will be URI escaped.
|
92
|
+
# @param [String] value
|
93
|
+
# The value of the querystring parameter to add. This value
|
94
|
+
# will be URI escaped.
|
95
|
+
#
|
96
|
+
def append_query_param(*args)
|
97
|
+
param =
|
98
|
+
case args.size
|
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
|
107
|
+
end
|
108
|
+
|
109
|
+
# Append a host prefix to the HTTP request URL.
|
110
|
+
#
|
111
|
+
# http_req.url = "https://example.com"
|
112
|
+
# http_req.prefix_host('data.')
|
113
|
+
#
|
114
|
+
# http_req.url
|
115
|
+
# #=> "https://data.foo.com
|
116
|
+
#
|
117
|
+
# @param [String] prefix A dot (.) terminated prefix for the host.
|
118
|
+
#
|
119
|
+
def prefix_host(prefix)
|
120
|
+
uri = URI.parse(@url)
|
121
|
+
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
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Hearth
|
6
|
+
module HTTP
|
7
|
+
# Represents an HTTP Response.
|
8
|
+
# @api private
|
9
|
+
class Response
|
10
|
+
# @param [Integer] status
|
11
|
+
# @param [Headers] headers
|
12
|
+
# @param [IO] body
|
13
|
+
def initialize(status: 200, headers: Headers.new, body: StringIO.new)
|
14
|
+
@status = status
|
15
|
+
@headers = headers
|
16
|
+
@body = body
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Integer]
|
20
|
+
attr_accessor :status
|
21
|
+
|
22
|
+
# @return [Headers]
|
23
|
+
attr_accessor :headers
|
24
|
+
|
25
|
+
# @return [IO]
|
26
|
+
attr_accessor :body
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|