patient_http 1.4.0 → 1.6.0
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/ARCHITECTURE.md +19 -7
- data/CHANGELOG.md +43 -0
- data/README.md +68 -7
- data/VERSION +1 -1
- data/lib/patient_http/client.rb +27 -2
- data/lib/patient_http/client_pool.rb +14 -7
- data/lib/patient_http/completion_executor.rb +139 -0
- data/lib/patient_http/configuration.rb +79 -1
- data/lib/patient_http/outgoing_request.rb +1 -1
- data/lib/patient_http/payload.rb +37 -15
- data/lib/patient_http/processor.rb +285 -95
- data/lib/patient_http/processor_observer.rb +36 -3
- data/lib/patient_http/redirect_helper.rb +84 -1
- data/lib/patient_http/request.rb +64 -6
- data/lib/patient_http/request_helper.rb +63 -10
- data/lib/patient_http/request_preparer.rb +7 -0
- data/lib/patient_http/request_task.rb +26 -12
- data/lib/patient_http/request_template.rb +46 -4
- data/lib/patient_http/response_reader.rb +233 -19
- data/lib/patient_http/synchronous_executor.rb +12 -71
- data/lib/patient_http.rb +49 -5
- metadata +3 -2
|
@@ -15,6 +15,16 @@ module PatientHttp
|
|
|
15
15
|
# @return [Integer] Maximum number of concurrent connections
|
|
16
16
|
attr_reader :max_connections
|
|
17
17
|
|
|
18
|
+
# @return [Integer, nil] Maximum number of connections per host (nil for unlimited)
|
|
19
|
+
attr_reader :max_connections_per_host
|
|
20
|
+
|
|
21
|
+
# @return [Integer] Number of threads that deliver completed results
|
|
22
|
+
attr_reader :completion_threads
|
|
23
|
+
|
|
24
|
+
# @return [Integer] Number of retries when delivering a completed result fails.
|
|
25
|
+
# A retry calls the task handler again, so handlers must be idempotent.
|
|
26
|
+
attr_reader :completion_retries
|
|
27
|
+
|
|
18
28
|
# @return [Numeric] Default request timeout in seconds
|
|
19
29
|
attr_reader :request_timeout
|
|
20
30
|
|
|
@@ -33,6 +43,15 @@ module PatientHttp
|
|
|
33
43
|
# @return [Integer] Maximum number of redirects to follow (0 disables redirects)
|
|
34
44
|
attr_reader :max_redirects
|
|
35
45
|
|
|
46
|
+
# @return [Boolean] Whether a redirect that requires changing the HTTP method
|
|
47
|
+
# (for example POST to GET on a 302) may be followed. When false, such a
|
|
48
|
+
# redirect response is returned as the result instead of being followed.
|
|
49
|
+
attr_reader :follow_method_changing_redirects
|
|
50
|
+
|
|
51
|
+
# @return [Array<String>] Lowercase header names that are always stripped from
|
|
52
|
+
# redirected requests
|
|
53
|
+
attr_reader :redirect_strip_headers
|
|
54
|
+
|
|
36
55
|
# @return [Integer] This is the maximum number of hosts for which connections
|
|
37
56
|
# will be kept alive for at one time.
|
|
38
57
|
attr_reader :connection_pool_size
|
|
@@ -63,11 +82,23 @@ module PatientHttp
|
|
|
63
82
|
# @param user_agent [String, nil] Default User-Agent header value
|
|
64
83
|
# @param raise_error_responses [Boolean] Whether to raise HttpError for non-2xx responses by default
|
|
65
84
|
# @param max_redirects [Integer] Maximum number of redirects to follow (0 disables redirects)
|
|
85
|
+
# @param follow_method_changing_redirects [Boolean] Whether to follow a redirect that requires changing the
|
|
86
|
+
# HTTP method, such as POST to GET on a 301, 302, or 303 response. When false, requests
|
|
87
|
+
# whose method would change do not follow the redirect and receive the redirect response.
|
|
88
|
+
# @param redirect_strip_headers [String, Array<String>] Header names (case insensitive)
|
|
89
|
+
# that are always stripped from redirected requests, so sensitive headers are never
|
|
90
|
+
# sent to a redirect target
|
|
66
91
|
# @param connection_pool_size [Integer] Maximum number of host clients to pool
|
|
67
92
|
# @param connection_timeout [Numeric, nil] Connection timeout in seconds
|
|
68
93
|
# @param proxy_url [String, nil] HTTP/HTTPS proxy URL (supports authentication)
|
|
69
94
|
# @param retries [Integer] Number of retries for failed requests
|
|
70
95
|
# @param protocol [Symbol, nil] HTTP protocol to use (:http1 or :http2); nil to negotiate
|
|
96
|
+
# @param max_connections_per_host [Integer, nil] Maximum number of connections per host (nil for unlimited)
|
|
97
|
+
# @param completion_threads [Integer] Number of threads that deliver completed results
|
|
98
|
+
# @param completion_retries [Integer] Number of retries when delivering a completed result fails.
|
|
99
|
+
# A retry calls TaskHandler#on_complete or #on_error again, so a handler that raises after
|
|
100
|
+
# its side effect delivers the callback more than once unless it is idempotent. Set to 0 to
|
|
101
|
+
# report the first failure without retrying.
|
|
71
102
|
def initialize(
|
|
72
103
|
max_connections: 256,
|
|
73
104
|
request_timeout: 60,
|
|
@@ -77,12 +108,17 @@ module PatientHttp
|
|
|
77
108
|
user_agent: "PatientHttp",
|
|
78
109
|
raise_error_responses: false,
|
|
79
110
|
max_redirects: 5,
|
|
111
|
+
follow_method_changing_redirects: true,
|
|
112
|
+
redirect_strip_headers: [],
|
|
80
113
|
connection_pool_size: 100,
|
|
81
114
|
connection_timeout: nil,
|
|
82
115
|
proxy_url: nil,
|
|
83
116
|
retries: 3,
|
|
84
117
|
protocol: nil,
|
|
85
|
-
encryption_key: nil
|
|
118
|
+
encryption_key: nil,
|
|
119
|
+
max_connections_per_host: nil,
|
|
120
|
+
completion_threads: 2,
|
|
121
|
+
completion_retries: 2
|
|
86
122
|
)
|
|
87
123
|
@mutex = Mutex.new
|
|
88
124
|
|
|
@@ -107,12 +143,17 @@ module PatientHttp
|
|
|
107
143
|
self.user_agent = user_agent
|
|
108
144
|
self.raise_error_responses = raise_error_responses
|
|
109
145
|
self.max_redirects = max_redirects
|
|
146
|
+
self.follow_method_changing_redirects = follow_method_changing_redirects
|
|
147
|
+
self.redirect_strip_headers = redirect_strip_headers
|
|
110
148
|
self.connection_pool_size = connection_pool_size
|
|
111
149
|
self.connection_timeout = connection_timeout
|
|
112
150
|
self.proxy_url = proxy_url
|
|
113
151
|
self.retries = retries
|
|
114
152
|
self.protocol = protocol
|
|
115
153
|
self.encryption_key = encryption_key
|
|
154
|
+
self.max_connections_per_host = max_connections_per_host
|
|
155
|
+
self.completion_threads = completion_threads
|
|
156
|
+
self.completion_retries = completion_retries
|
|
116
157
|
end
|
|
117
158
|
|
|
118
159
|
# Get the logger to use to report pool events. Default is to log errors to STDERR.
|
|
@@ -124,6 +165,26 @@ module PatientHttp
|
|
|
124
165
|
@max_connections = value
|
|
125
166
|
end
|
|
126
167
|
|
|
168
|
+
def max_connections_per_host=(value)
|
|
169
|
+
if value.nil?
|
|
170
|
+
@max_connections_per_host = nil
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
validate_positive_integer(:max_connections_per_host, value)
|
|
175
|
+
@max_connections_per_host = value
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def completion_threads=(value)
|
|
179
|
+
validate_positive_integer(:completion_threads, value)
|
|
180
|
+
@completion_threads = value
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def completion_retries=(value)
|
|
184
|
+
validate_non_negative_integer(:completion_retries, value)
|
|
185
|
+
@completion_retries = value
|
|
186
|
+
end
|
|
187
|
+
|
|
127
188
|
def request_timeout=(value)
|
|
128
189
|
validate_positive(:request_timeout, value)
|
|
129
190
|
@request_timeout = value
|
|
@@ -144,6 +205,18 @@ module PatientHttp
|
|
|
144
205
|
@max_redirects = value
|
|
145
206
|
end
|
|
146
207
|
|
|
208
|
+
def follow_method_changing_redirects=(value)
|
|
209
|
+
unless value == true || value == false
|
|
210
|
+
raise ArgumentError.new("follow_method_changing_redirects must be true or false, got: #{value.inspect}")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@follow_method_changing_redirects = value
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def redirect_strip_headers=(value)
|
|
217
|
+
@redirect_strip_headers = RedirectHelper.normalize_header_names(value)
|
|
218
|
+
end
|
|
219
|
+
|
|
147
220
|
def connection_pool_size=(value)
|
|
148
221
|
validate_positive_integer(:connection_pool_size, value)
|
|
149
222
|
@connection_pool_size = value
|
|
@@ -383,11 +456,16 @@ module PatientHttp
|
|
|
383
456
|
"user_agent" => user_agent,
|
|
384
457
|
"raise_error_responses" => raise_error_responses,
|
|
385
458
|
"max_redirects" => max_redirects,
|
|
459
|
+
"follow_method_changing_redirects" => follow_method_changing_redirects,
|
|
460
|
+
"redirect_strip_headers" => redirect_strip_headers,
|
|
386
461
|
"connection_pool_size" => connection_pool_size,
|
|
387
462
|
"connection_timeout" => connection_timeout,
|
|
388
463
|
"proxy_url" => proxy_url,
|
|
389
464
|
"retries" => retries,
|
|
390
465
|
"protocol" => protocol,
|
|
466
|
+
"max_connections_per_host" => max_connections_per_host,
|
|
467
|
+
"completion_threads" => completion_threads,
|
|
468
|
+
"completion_retries" => completion_retries,
|
|
391
469
|
"payload_stores" => payload_stores.keys,
|
|
392
470
|
"default_payload_store" => default_payload_store_name,
|
|
393
471
|
"secrets" => @mutex.synchronize { @secrets.keys },
|
|
@@ -12,7 +12,7 @@ module PatientHttp
|
|
|
12
12
|
#
|
|
13
13
|
# @see Configuration#register_preprocessor
|
|
14
14
|
class OutgoingRequest
|
|
15
|
-
# @return [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
|
|
15
|
+
# @return [Symbol] HTTP method (:get, :head, :post, :put, :patch, :delete, :query)
|
|
16
16
|
attr_reader :http_method
|
|
17
17
|
|
|
18
18
|
# @return [String] the request URL with any secret query params already resolved
|
data/lib/patient_http/payload.rb
CHANGED
|
@@ -29,7 +29,9 @@ module PatientHttp
|
|
|
29
29
|
# Encodes a value based on its MIME type.
|
|
30
30
|
#
|
|
31
31
|
# For text-based content types, applies gzip compression if beneficial.
|
|
32
|
-
# For binary content, uses Base64 encoding.
|
|
32
|
+
# For binary content, uses Base64 encoding. A value that a text MIME type
|
|
33
|
+
# claims is text but that does not hold text is encoded as binary as
|
|
34
|
+
# well, because the serialized form must survive JSON encoding.
|
|
33
35
|
#
|
|
34
36
|
# @param value [String] the value to encode
|
|
35
37
|
# @param mimetype [String, nil] the MIME type of the content
|
|
@@ -38,21 +40,11 @@ module PatientHttp
|
|
|
38
40
|
return nil if value.nil?
|
|
39
41
|
|
|
40
42
|
if is_text_mimetype?(mimetype)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if value.bytesize < 4096
|
|
44
|
-
[:text, value, value.encoding.name]
|
|
45
|
-
else
|
|
46
|
-
gzipped = Zlib.gzip(value)
|
|
47
|
-
if gzipped.bytesize < value.bytesize
|
|
48
|
-
[:gzipped, [gzipped].pack("m0"), value.encoding.name]
|
|
49
|
-
else
|
|
50
|
-
[:text, value, value.encoding.name]
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
else
|
|
54
|
-
[:binary, [value].pack("m0"), Encoding::BINARY.name]
|
|
43
|
+
text = text_value(value, charset(mimetype))
|
|
44
|
+
return encode_text(text) if text?(text)
|
|
55
45
|
end
|
|
46
|
+
|
|
47
|
+
[:binary, [value].pack("m0"), Encoding::BINARY.name]
|
|
56
48
|
end
|
|
57
49
|
|
|
58
50
|
# Decodes an encoded value based on its encoding type.
|
|
@@ -78,6 +70,36 @@ module PatientHttp
|
|
|
78
70
|
|
|
79
71
|
private
|
|
80
72
|
|
|
73
|
+
# Encode a text value, compressing it when that makes it smaller.
|
|
74
|
+
#
|
|
75
|
+
# @param value [String] the text to encode
|
|
76
|
+
# @return [Array(Symbol, String, String)] [encoding, encoded_value, charset]
|
|
77
|
+
def encode_text(value)
|
|
78
|
+
return [:text, value, value.encoding.name] if value.bytesize < 4096
|
|
79
|
+
|
|
80
|
+
gzipped = Zlib.gzip(value)
|
|
81
|
+
if gzipped.bytesize < value.bytesize
|
|
82
|
+
[:gzipped, [gzipped].pack("m0"), value.encoding.name]
|
|
83
|
+
else
|
|
84
|
+
[:text, value, value.encoding.name]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Whether a value can be serialized as text. JSON encoding converts a
|
|
89
|
+
# string to UTF-8, so the value must either be valid text in its own
|
|
90
|
+
# encoding or hold bytes that are already valid UTF-8. A body still
|
|
91
|
+
# carrying a content encoding the reader could not decode holds neither,
|
|
92
|
+
# even though its MIME type names a text type.
|
|
93
|
+
#
|
|
94
|
+
# @param value [String] the value to check
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def text?(value)
|
|
97
|
+
return value.valid_encoding? unless value.encoding == Encoding::BINARY
|
|
98
|
+
return true if value.ascii_only?
|
|
99
|
+
|
|
100
|
+
value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
101
|
+
end
|
|
102
|
+
|
|
81
103
|
def is_text_mimetype?(mimetype)
|
|
82
104
|
mimetype&.match?(/\Atext\/|application\/(?:json|xml|javascript)/)
|
|
83
105
|
end
|