http 6.0.0-java
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 +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/capture_warning.rb +10 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/test/support/fakeio.rb +21 -0
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +218 -0
data/lib/http/session.rb
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
require "http/cookie_jar"
|
|
6
|
+
require "http/headers"
|
|
7
|
+
require "http/redirector"
|
|
8
|
+
require "http/request/builder"
|
|
9
|
+
|
|
10
|
+
module HTTP
|
|
11
|
+
# Thread-safe options builder for configuring HTTP requests.
|
|
12
|
+
#
|
|
13
|
+
# Session objects are returned by all chainable configuration methods
|
|
14
|
+
# (e.g., {Chainable#headers}, {Chainable#timeout}, {Chainable#cookies}).
|
|
15
|
+
# They hold an immutable {Options} object and create a new {Client}
|
|
16
|
+
# for each request, making them safe to share across threads.
|
|
17
|
+
#
|
|
18
|
+
# When configured for persistent connections (via {Chainable#persistent}),
|
|
19
|
+
# the session maintains a pool of {Client} instances keyed by origin,
|
|
20
|
+
# enabling connection reuse within the same origin and transparent
|
|
21
|
+
# cross-origin redirect handling.
|
|
22
|
+
#
|
|
23
|
+
# @example Reuse a configured session across threads
|
|
24
|
+
# session = HTTP.headers("Accept" => "application/json").timeout(10)
|
|
25
|
+
# threads = 5.times.map do
|
|
26
|
+
# Thread.new { session.get("https://example.com") }
|
|
27
|
+
# end
|
|
28
|
+
# threads.each(&:join)
|
|
29
|
+
#
|
|
30
|
+
# @example Persistent session with cross-origin redirects
|
|
31
|
+
# HTTP.persistent("https://example.com").follow do |http|
|
|
32
|
+
# http.get("/redirect-to-other-domain") # follows cross-origin redirect
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @see Chainable
|
|
36
|
+
# @see Client
|
|
37
|
+
class Session
|
|
38
|
+
extend Forwardable
|
|
39
|
+
include Chainable
|
|
40
|
+
|
|
41
|
+
# @!method persistent?
|
|
42
|
+
# Indicate whether the session has persistent connection options
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# session = HTTP::Session.new(persistent: "http://example.com")
|
|
46
|
+
# session.persistent?
|
|
47
|
+
#
|
|
48
|
+
# @see Options#persistent?
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
# @api public
|
|
51
|
+
def_delegator :default_options, :persistent?
|
|
52
|
+
|
|
53
|
+
# Initialize a new Session
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# session = HTTP::Session.new(headers: {"Accept" => "application/json"})
|
|
57
|
+
#
|
|
58
|
+
# @param default_options [HTTP::Options, nil] existing options instance
|
|
59
|
+
# @param clients [Hash, nil] shared connection pool (internal use)
|
|
60
|
+
# @param options [Hash] keyword options (see HTTP::Options#initialize)
|
|
61
|
+
# @return [HTTP::Session] a new session instance
|
|
62
|
+
# @api public
|
|
63
|
+
def initialize(default_options = nil, clients: nil, **)
|
|
64
|
+
@default_options = HTTP::Options.new(default_options, **)
|
|
65
|
+
@clients = clients || {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Close all persistent connections held by this session
|
|
69
|
+
#
|
|
70
|
+
# When the session is persistent, this closes every pooled {Client}
|
|
71
|
+
# and clears the pool. Safe to call on non-persistent sessions (no-op).
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# session = HTTP.persistent("https://example.com")
|
|
75
|
+
# session.get("/")
|
|
76
|
+
# session.close
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
# @api public
|
|
80
|
+
def close
|
|
81
|
+
@clients.each_value(&:close)
|
|
82
|
+
@clients.clear
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Make an HTTP request
|
|
86
|
+
#
|
|
87
|
+
# For non-persistent sessions a fresh {Client} is created for each
|
|
88
|
+
# request, ensuring thread safety. For persistent sessions the pooled
|
|
89
|
+
# {Client} for the request's origin is reused.
|
|
90
|
+
#
|
|
91
|
+
# Manages cookies across redirect hops when following redirects.
|
|
92
|
+
#
|
|
93
|
+
# @example Without a block
|
|
94
|
+
# session = HTTP::Session.new
|
|
95
|
+
# session.request(:get, "https://example.com")
|
|
96
|
+
#
|
|
97
|
+
# @example With a block (auto-closes connection)
|
|
98
|
+
# session = HTTP::Session.new
|
|
99
|
+
# session.request(:get, "https://example.com") { |res| res.status }
|
|
100
|
+
#
|
|
101
|
+
# @param verb [Symbol] the HTTP method
|
|
102
|
+
# @param uri [#to_s] the URI to request
|
|
103
|
+
# @yieldparam response [HTTP::Response] the response
|
|
104
|
+
# @return [HTTP::Response, Object] the response, or block return value
|
|
105
|
+
# @api public
|
|
106
|
+
def request(verb, uri,
|
|
107
|
+
headers: nil, params: nil, form: nil, json: nil, body: nil,
|
|
108
|
+
response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
|
|
109
|
+
proxy: nil, nodelay: nil, features: nil, retriable: nil,
|
|
110
|
+
socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
|
|
111
|
+
timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil, &block)
|
|
112
|
+
merged = default_options.merge(
|
|
113
|
+
{ headers: headers, params: params, form: form, json: json, body: body,
|
|
114
|
+
response: response, encoding: encoding, follow: follow, ssl: ssl,
|
|
115
|
+
ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
|
|
116
|
+
retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
|
|
117
|
+
timeout_class: timeout_class, timeout_options: timeout_options,
|
|
118
|
+
keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
|
|
119
|
+
)
|
|
120
|
+
client = persistent? ? nil : make_client(default_options)
|
|
121
|
+
res = perform_request(client, verb, uri, merged)
|
|
122
|
+
|
|
123
|
+
return res unless block
|
|
124
|
+
|
|
125
|
+
yield res
|
|
126
|
+
ensure
|
|
127
|
+
if block
|
|
128
|
+
persistent? ? close : client&.close
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Create a new session with the given options
|
|
135
|
+
#
|
|
136
|
+
# When the current session is persistent, the child session shares the
|
|
137
|
+
# same connection pool so that chaining methods like {Chainable#headers}
|
|
138
|
+
# or {Chainable#auth} do not break connection reuse.
|
|
139
|
+
#
|
|
140
|
+
# @param options [HTTP::Options] options for the new session
|
|
141
|
+
# @return [HTTP::Session]
|
|
142
|
+
# @api private
|
|
143
|
+
def branch(options)
|
|
144
|
+
if persistent?
|
|
145
|
+
self.class.new(options, clients: @clients)
|
|
146
|
+
else
|
|
147
|
+
self.class.new(options)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Execute a request with cookie management
|
|
152
|
+
#
|
|
153
|
+
# @param client [HTTP::Client, nil] the client (nil when persistent; looked up from pool)
|
|
154
|
+
# @param verb [Symbol] the HTTP method
|
|
155
|
+
# @param uri [#to_s] the URI to request
|
|
156
|
+
# @param merged [HTTP::Options] the merged options
|
|
157
|
+
# @return [HTTP::Response] the response
|
|
158
|
+
# @api private
|
|
159
|
+
def perform_request(client, verb, uri, merged)
|
|
160
|
+
cookie_jar = CookieJar.new
|
|
161
|
+
builder = Request::Builder.new(merged)
|
|
162
|
+
req = builder.build(verb, uri)
|
|
163
|
+
client ||= client_for_origin(req.uri.origin)
|
|
164
|
+
load_cookies(cookie_jar, req)
|
|
165
|
+
res = client.perform(req, merged)
|
|
166
|
+
store_cookies(cookie_jar, res)
|
|
167
|
+
|
|
168
|
+
return res unless merged.follow
|
|
169
|
+
|
|
170
|
+
perform_redirects(cookie_jar, client, req, res, merged)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Follow redirects with cookie management
|
|
174
|
+
#
|
|
175
|
+
# For persistent sessions, each redirect hop may target a different
|
|
176
|
+
# origin. The session looks up (or creates) a pooled {Client} for
|
|
177
|
+
# the redirect target's origin, allowing cross-origin redirects
|
|
178
|
+
# without raising {StateError}.
|
|
179
|
+
#
|
|
180
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
181
|
+
# @param client [HTTP::Client] the client for the initial request
|
|
182
|
+
# @param req [HTTP::Request] the original request
|
|
183
|
+
# @param res [HTTP::Response] the initial redirect response
|
|
184
|
+
# @param opts [HTTP::Options] the merged options
|
|
185
|
+
# @return [HTTP::Response] the final non-redirect response
|
|
186
|
+
# @api private
|
|
187
|
+
def perform_redirects(jar, client, req, res, opts)
|
|
188
|
+
builder = Request::Builder.new(opts)
|
|
189
|
+
follow = opts.follow || {} #: Hash[untyped, untyped]
|
|
190
|
+
Redirector.new(**follow).perform(req, res) do |redirect_req|
|
|
191
|
+
wrapped = builder.wrap(redirect_req)
|
|
192
|
+
apply_cookies(jar, wrapped)
|
|
193
|
+
apply_cookies(jar, redirect_req)
|
|
194
|
+
response = redirect_client(client, wrapped).perform(wrapped, opts)
|
|
195
|
+
store_cookies(jar, response)
|
|
196
|
+
response
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Return the appropriate client for a redirect hop
|
|
201
|
+
#
|
|
202
|
+
# @param client [HTTP::Client] the client for the original request
|
|
203
|
+
# @param request [HTTP::Request] the redirect request
|
|
204
|
+
# @return [HTTP::Client] the client for the redirect target
|
|
205
|
+
# @api private
|
|
206
|
+
def redirect_client(client, request)
|
|
207
|
+
persistent? ? client_for_origin(request.uri.origin) : client
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Return a pooled persistent {Client} for the given origin
|
|
211
|
+
#
|
|
212
|
+
# Creates a new {Client} if one does not already exist for this origin.
|
|
213
|
+
# For the session's primary persistent origin, the default options are
|
|
214
|
+
# used directly. For other origins (e.g. redirect targets), the
|
|
215
|
+
# persistent origin is overridden and base_uri is cleared.
|
|
216
|
+
#
|
|
217
|
+
# @param origin [String] the URI origin (scheme + host + port)
|
|
218
|
+
# @return [HTTP::Client] a persistent client for the origin
|
|
219
|
+
# @api private
|
|
220
|
+
def client_for_origin(origin)
|
|
221
|
+
@clients[origin] ||= make_client(options_for_origin(origin))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Build {Options} for a persistent client targeting the given origin
|
|
225
|
+
#
|
|
226
|
+
# @param origin [String] the URI origin
|
|
227
|
+
# @return [HTTP::Options] options configured for this origin
|
|
228
|
+
# @api private
|
|
229
|
+
def options_for_origin(origin)
|
|
230
|
+
return default_options if origin == default_options.persistent
|
|
231
|
+
|
|
232
|
+
default_options.merge(persistent: origin, base_uri: nil)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Load cookies from the request's Cookie header into the jar
|
|
236
|
+
#
|
|
237
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
238
|
+
# @param request [HTTP::Request] the request
|
|
239
|
+
# @return [void]
|
|
240
|
+
# @api private
|
|
241
|
+
def load_cookies(jar, request)
|
|
242
|
+
header = request.headers[Headers::COOKIE]
|
|
243
|
+
cookies = HTTP::Cookie.cookie_value_to_hash(header.to_s)
|
|
244
|
+
|
|
245
|
+
cookies.each do |name, value|
|
|
246
|
+
jar.add(HTTP::Cookie.new(name, value, path: request.uri.path, domain: request.host))
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Store cookies from the response's Set-Cookie headers into the jar
|
|
251
|
+
#
|
|
252
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
253
|
+
# @param response [HTTP::Response] the response
|
|
254
|
+
# @return [void]
|
|
255
|
+
# @api private
|
|
256
|
+
def store_cookies(jar, response)
|
|
257
|
+
response.cookies.each do |cookie|
|
|
258
|
+
if cookie.value == ""
|
|
259
|
+
jar.delete(cookie)
|
|
260
|
+
else
|
|
261
|
+
jar.add(cookie)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Apply cookies from the jar to the request's Cookie header
|
|
267
|
+
#
|
|
268
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
269
|
+
# @param request [HTTP::Request] the request
|
|
270
|
+
# @return [void]
|
|
271
|
+
# @api private
|
|
272
|
+
def apply_cookies(jar, request)
|
|
273
|
+
if jar.empty?
|
|
274
|
+
request.headers.delete(Headers::COOKIE)
|
|
275
|
+
else
|
|
276
|
+
request.headers.set(Headers::COOKIE, jar.map { |c| "#{c.name}=#{c.value}" }.join("; "))
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require "io/wait"
|
|
5
|
+
|
|
6
|
+
require "http/timeout/null"
|
|
7
|
+
|
|
8
|
+
module HTTP
|
|
9
|
+
module Timeout
|
|
10
|
+
# Timeout handler with a single global timeout for the entire request
|
|
11
|
+
class Global < Null
|
|
12
|
+
# I/O wait result symbols returned by non-blocking operations
|
|
13
|
+
WAIT_RESULTS = %i[wait_readable wait_writable].freeze
|
|
14
|
+
# Initializes global timeout with options
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# HTTP::Timeout::Global.new(global_timeout: 5)
|
|
18
|
+
#
|
|
19
|
+
# @param [Numeric] global_timeout Global timeout in seconds
|
|
20
|
+
# @param [Numeric, nil] read_timeout Read timeout in seconds
|
|
21
|
+
# @param [Numeric, nil] write_timeout Write timeout in seconds
|
|
22
|
+
# @param [Numeric, nil] connect_timeout Connect timeout in seconds
|
|
23
|
+
# @api public
|
|
24
|
+
# @return [HTTP::Timeout::Global]
|
|
25
|
+
def initialize(global_timeout:, read_timeout: nil, write_timeout: nil, connect_timeout: nil)
|
|
26
|
+
super
|
|
27
|
+
|
|
28
|
+
@timeout = @time_left = global_timeout
|
|
29
|
+
@read_timeout = read_timeout
|
|
30
|
+
@write_timeout = write_timeout
|
|
31
|
+
@connect_timeout = connect_timeout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Resets the time left counter to initial timeout
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# timeout.reset_counter
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
40
|
+
# @return [Numeric]
|
|
41
|
+
def reset_counter
|
|
42
|
+
@time_left = @timeout
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Connects to a socket with global timeout
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# timeout.connect(TCPSocket, "example.com", 80)
|
|
49
|
+
#
|
|
50
|
+
# @param [Class] socket_class
|
|
51
|
+
# @param [String] host
|
|
52
|
+
# @param [Integer] port
|
|
53
|
+
# @param [Boolean] nodelay
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [void]
|
|
56
|
+
def connect(socket_class, host, port, nodelay: false)
|
|
57
|
+
reset_timer
|
|
58
|
+
@socket = open_socket(socket_class, host, port, connect_timeout: effective_timeout(@connect_timeout))
|
|
59
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
|
60
|
+
|
|
61
|
+
log_time
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Starts an SSL connection on a socket
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# timeout.connect_ssl
|
|
68
|
+
#
|
|
69
|
+
# @api public
|
|
70
|
+
# @return [void]
|
|
71
|
+
def connect_ssl
|
|
72
|
+
reset_timer
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
@socket.connect_nonblock
|
|
76
|
+
rescue IO::WaitReadable
|
|
77
|
+
wait_readable_or_timeout(@connect_timeout)
|
|
78
|
+
retry
|
|
79
|
+
rescue IO::WaitWritable
|
|
80
|
+
wait_writable_or_timeout(@connect_timeout)
|
|
81
|
+
retry
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Read from the socket
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# timeout.readpartial(1024)
|
|
89
|
+
#
|
|
90
|
+
# @param [Integer] size
|
|
91
|
+
# @param [String, nil] buffer
|
|
92
|
+
# @api public
|
|
93
|
+
# @return [String, :eof]
|
|
94
|
+
def readpartial(size, buffer = nil)
|
|
95
|
+
perform_io(@read_timeout) { read_nonblock(size, buffer) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Write to the socket
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# timeout.write("GET / HTTP/1.1")
|
|
102
|
+
#
|
|
103
|
+
# @param [String] data
|
|
104
|
+
# @api public
|
|
105
|
+
# @return [Integer, :eof]
|
|
106
|
+
def write(data)
|
|
107
|
+
perform_io(@write_timeout) { write_nonblock(data) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
alias << write
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Reads from socket in non-blocking mode
|
|
115
|
+
#
|
|
116
|
+
# @api private
|
|
117
|
+
# @return [String, Symbol]
|
|
118
|
+
def read_nonblock(size, buffer = nil)
|
|
119
|
+
@socket.read_nonblock(size, buffer, exception: false)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Writes to socket in non-blocking mode
|
|
123
|
+
#
|
|
124
|
+
# @api private
|
|
125
|
+
# @return [Integer, Symbol]
|
|
126
|
+
def write_nonblock(data)
|
|
127
|
+
@socket.write_nonblock(data, exception: false)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Performs I/O operation with timeout tracking
|
|
131
|
+
#
|
|
132
|
+
# @param [Numeric, nil] per_op_timeout per-operation timeout limit
|
|
133
|
+
# @api private
|
|
134
|
+
# @return [Object]
|
|
135
|
+
def perform_io(per_op_timeout = nil)
|
|
136
|
+
reset_timer
|
|
137
|
+
|
|
138
|
+
loop do
|
|
139
|
+
result = yield
|
|
140
|
+
return handle_io_result(result) unless WAIT_RESULTS.include?(result)
|
|
141
|
+
|
|
142
|
+
wait_for_io(result, per_op_timeout)
|
|
143
|
+
rescue IO::WaitReadable then wait_readable_or_timeout(per_op_timeout)
|
|
144
|
+
rescue IO::WaitWritable then wait_writable_or_timeout(per_op_timeout)
|
|
145
|
+
end
|
|
146
|
+
rescue EOFError
|
|
147
|
+
:eof
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Handles the result of an I/O operation
|
|
151
|
+
#
|
|
152
|
+
# @api private
|
|
153
|
+
# @return [Object, Symbol]
|
|
154
|
+
def handle_io_result(result)
|
|
155
|
+
result.nil? ? :eof : result
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Waits for an I/O readiness based on the result type
|
|
159
|
+
#
|
|
160
|
+
# @param [Symbol] result the I/O wait type
|
|
161
|
+
# @param [Numeric, nil] per_op_timeout per-operation timeout limit
|
|
162
|
+
# @api private
|
|
163
|
+
# @return [void]
|
|
164
|
+
def wait_for_io(result, per_op_timeout = nil)
|
|
165
|
+
if result == :wait_readable
|
|
166
|
+
wait_readable_or_timeout(per_op_timeout)
|
|
167
|
+
else
|
|
168
|
+
wait_writable_or_timeout(per_op_timeout)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Waits for a socket to become readable
|
|
173
|
+
#
|
|
174
|
+
# @param [Numeric, nil] per_op per-operation timeout limit
|
|
175
|
+
# @api private
|
|
176
|
+
# @return [void]
|
|
177
|
+
def wait_readable_or_timeout(per_op = nil)
|
|
178
|
+
timeout = effective_timeout(per_op)
|
|
179
|
+
result = @socket.to_io.wait_readable(timeout)
|
|
180
|
+
log_time
|
|
181
|
+
|
|
182
|
+
raise TimeoutError, "Read timed out after #{per_op} seconds" if per_op && result.nil?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Waits for a socket to become writable
|
|
186
|
+
#
|
|
187
|
+
# @param [Numeric, nil] per_op per-operation timeout limit
|
|
188
|
+
# @api private
|
|
189
|
+
# @return [void]
|
|
190
|
+
def wait_writable_or_timeout(per_op = nil)
|
|
191
|
+
timeout = effective_timeout(per_op)
|
|
192
|
+
result = @socket.to_io.wait_writable(timeout)
|
|
193
|
+
log_time
|
|
194
|
+
|
|
195
|
+
raise TimeoutError, "Write timed out after #{per_op} seconds" if per_op && result.nil?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Computes the effective timeout as the minimum of global and per-operation
|
|
199
|
+
#
|
|
200
|
+
# @param [Numeric, nil] per_op_timeout per-operation timeout limit
|
|
201
|
+
# @api private
|
|
202
|
+
# @return [Numeric]
|
|
203
|
+
def effective_timeout(per_op_timeout)
|
|
204
|
+
return @time_left unless per_op_timeout
|
|
205
|
+
|
|
206
|
+
[per_op_timeout, @time_left].min
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Resets the I/O timer to current time
|
|
210
|
+
#
|
|
211
|
+
# @api private
|
|
212
|
+
# @return [Time]
|
|
213
|
+
def reset_timer
|
|
214
|
+
@started = Time.now
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Logs elapsed time and checks for timeout
|
|
218
|
+
#
|
|
219
|
+
# @api private
|
|
220
|
+
# @return [void]
|
|
221
|
+
def log_time
|
|
222
|
+
@time_left -= (Time.now - @started)
|
|
223
|
+
raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
|
|
224
|
+
|
|
225
|
+
reset_timer
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|