hearth 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|