httpx 1.6.3 → 1.7.1
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/doc/release_notes/0_11_0.md +3 -3
- data/doc/release_notes/1_6_3.md +2 -2
- data/doc/release_notes/1_7_0.md +149 -0
- data/doc/release_notes/1_7_1.md +21 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/webmock.rb +18 -9
- data/lib/httpx/altsvc.rb +4 -2
- data/lib/httpx/connection/http1.rb +9 -9
- data/lib/httpx/connection/http2.rb +2 -0
- data/lib/httpx/connection.rb +7 -9
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/tcp.rb +1 -1
- data/lib/httpx/loggable.rb +2 -0
- data/lib/httpx/options.rb +118 -22
- data/lib/httpx/parser/http1.rb +1 -0
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +113 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
- data/lib/httpx/plugins/cookies/cookie.rb +1 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -5
- data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +156 -57
- data/lib/httpx/plugins/persistent.rb +3 -5
- data/lib/httpx/plugins/proxy/http.rb +0 -4
- data/lib/httpx/plugins/proxy.rb +3 -1
- data/lib/httpx/plugins/query.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +20 -15
- data/lib/httpx/plugins/response_cache.rb +3 -7
- data/lib/httpx/plugins/retries.rb +60 -24
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +60 -9
- data/lib/httpx/plugins/stream_bidi.rb +84 -16
- data/lib/httpx/pool.rb +12 -3
- data/lib/httpx/request/body.rb +1 -1
- data/lib/httpx/request.rb +10 -1
- data/lib/httpx/resolver/cache/base.rb +136 -0
- data/lib/httpx/resolver/cache/memory.rb +42 -0
- data/lib/httpx/resolver/cache.rb +18 -0
- data/lib/httpx/resolver/https.rb +74 -20
- data/lib/httpx/resolver/multi.rb +10 -2
- data/lib/httpx/resolver/native.rb +32 -6
- data/lib/httpx/resolver/resolver.rb +3 -3
- data/lib/httpx/resolver.rb +36 -114
- data/lib/httpx/response/body.rb +5 -3
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +14 -3
- data/lib/httpx/session.rb +6 -6
- data/lib/httpx/timers.rb +6 -12
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/gzip.rb +7 -2
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +5 -5
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +17 -9
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/utils.rb +13 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +9 -3
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +1 -3
- data/sig/loggable.rbs +1 -1
- data/sig/options.rbs +12 -4
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +37 -4
- data/sig/plugins/basic_auth.rbs +3 -3
- data/sig/plugins/digest_auth.rbs +2 -4
- data/sig/plugins/fiber_concurrency.rbs +6 -0
- data/sig/plugins/ntlm_auth.rbs +2 -2
- data/sig/plugins/oauth.rbs +44 -15
- data/sig/plugins/rate_limiter.rbs +4 -2
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +12 -4
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/resolver/cache/base.rbs +28 -0
- data/sig/resolver/cache/memory.rbs +13 -0
- data/sig/resolver/cache.rbs +16 -0
- data/sig/resolver/https.rbs +24 -0
- data/sig/resolver/multi.rbs +8 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver.rbs +5 -20
- data/sig/response.rbs +3 -0
- data/sig/session.rbs +3 -5
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- data/sig/utils.rbs +2 -0
- metadata +11 -1
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -55,9 +55,9 @@ module HTTPX
|
|
|
55
55
|
line << chunk
|
|
56
56
|
|
|
57
57
|
while (idx = line.index("\n"))
|
|
58
|
-
yield line.byteslice(0..idx - 1)
|
|
58
|
+
yield line.byteslice(0..(idx - 1))
|
|
59
59
|
|
|
60
|
-
line = line.byteslice(idx + 1..-1)
|
|
60
|
+
line = line.byteslice((idx + 1)..-1)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -121,20 +121,71 @@ module HTTPX
|
|
|
121
121
|
# https://gitlab.com/os85/httpx/wikis/Stream
|
|
122
122
|
#
|
|
123
123
|
module Stream
|
|
124
|
+
STREAM_REQUEST_OPTIONS = { timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 }.freeze }.freeze
|
|
125
|
+
|
|
124
126
|
def self.extra_options(options)
|
|
125
|
-
options.merge(
|
|
127
|
+
options.merge(
|
|
128
|
+
stream: false,
|
|
129
|
+
timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 },
|
|
130
|
+
stream_response_class: Class.new(StreamResponse, &Options::SET_TEMPORARY_NAME).freeze
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# adds support for the following options:
|
|
135
|
+
#
|
|
136
|
+
# :stream :: whether the request to process should be handled as a stream (defaults to <tt>false</tt>).
|
|
137
|
+
# :stream_response_class :: Class used to build the stream response object.
|
|
138
|
+
module OptionsMethods
|
|
139
|
+
def option_stream(val)
|
|
140
|
+
val
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def option_stream_response_class(value)
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def extend_with_plugin_classes(pl)
|
|
148
|
+
return super unless defined?(pl::StreamResponseMethods)
|
|
149
|
+
|
|
150
|
+
@stream_response_class = @stream_response_class.dup
|
|
151
|
+
Options::SET_TEMPORARY_NAME[@stream_response_class, pl]
|
|
152
|
+
@stream_response_class.__send__(:include, pl::StreamResponseMethods) if defined?(pl::StreamResponseMethods)
|
|
153
|
+
|
|
154
|
+
super
|
|
155
|
+
end
|
|
126
156
|
end
|
|
127
157
|
|
|
128
158
|
module InstanceMethods
|
|
129
|
-
def request(*args,
|
|
130
|
-
|
|
159
|
+
def request(*args, **options)
|
|
160
|
+
if args.first.is_a?(Request)
|
|
161
|
+
requests = args
|
|
162
|
+
|
|
163
|
+
request = requests.first
|
|
164
|
+
|
|
165
|
+
unless request.options.stream && !request.stream
|
|
166
|
+
if options[:stream]
|
|
167
|
+
warn "passing `stream: true` with a request object is not supported anymore. " \
|
|
168
|
+
"You can instead build the request object with `stream :true`"
|
|
169
|
+
end
|
|
170
|
+
return super
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
return super unless options[:stream]
|
|
174
|
+
|
|
175
|
+
requests = build_requests(*args, options)
|
|
176
|
+
|
|
177
|
+
request = requests.first
|
|
178
|
+
end
|
|
131
179
|
|
|
132
|
-
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
|
133
180
|
raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
|
|
134
181
|
|
|
135
|
-
request
|
|
182
|
+
@options.stream_response_class.new(request, self)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_request(verb, uri, params = EMPTY_HASH, options = @options)
|
|
186
|
+
return super unless params[:stream]
|
|
136
187
|
|
|
137
|
-
|
|
188
|
+
super(verb, uri, params, options.merge(STREAM_REQUEST_OPTIONS.merge(stream: true)))
|
|
138
189
|
end
|
|
139
190
|
end
|
|
140
191
|
|
|
@@ -166,7 +217,7 @@ module HTTPX
|
|
|
166
217
|
|
|
167
218
|
@stream.on_chunk(chunk.dup)
|
|
168
219
|
|
|
169
|
-
chunk.
|
|
220
|
+
chunk.bytesize
|
|
170
221
|
end
|
|
171
222
|
|
|
172
223
|
private
|
|
@@ -26,6 +26,8 @@ module HTTPX
|
|
|
26
26
|
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
27
27
|
# lock.aware version of +#{lock_meth}+
|
|
28
28
|
def #{lock_meth}(*) # def close(*)
|
|
29
|
+
return super unless @options.stream
|
|
30
|
+
|
|
29
31
|
return super if @lock.owned?
|
|
30
32
|
|
|
31
33
|
# small race condition between
|
|
@@ -43,6 +45,8 @@ module HTTPX
|
|
|
43
45
|
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
44
46
|
# lock.aware version of +#{lock_meth}+
|
|
45
47
|
private def #{lock_meth}(*) # private def join_headers(*)
|
|
48
|
+
return super unless @options.stream
|
|
49
|
+
|
|
46
50
|
return super if @lock.owned?
|
|
47
51
|
|
|
48
52
|
# small race condition between
|
|
@@ -55,6 +59,8 @@ module HTTPX
|
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def handle_stream(stream, request)
|
|
62
|
+
return super unless @options.stream
|
|
63
|
+
|
|
58
64
|
request.on(:body) do
|
|
59
65
|
next unless request.headers_sent
|
|
60
66
|
|
|
@@ -67,6 +73,8 @@ module HTTPX
|
|
|
67
73
|
|
|
68
74
|
# when there ain't more chunks, it makes the buffer as full.
|
|
69
75
|
def send_chunk(request, stream, chunk, next_chunk)
|
|
76
|
+
return super unless @options.stream
|
|
77
|
+
|
|
70
78
|
super
|
|
71
79
|
|
|
72
80
|
return if next_chunk
|
|
@@ -77,39 +85,60 @@ module HTTPX
|
|
|
77
85
|
|
|
78
86
|
# sets end-stream flag when the request is closed.
|
|
79
87
|
def end_stream?(request, next_chunk)
|
|
88
|
+
return super unless @options.stream
|
|
89
|
+
|
|
80
90
|
request.closed? && next_chunk.nil?
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
# BidiBuffer is a Buffer which can
|
|
85
|
-
# than the thread of the corresponding Connection/Session.
|
|
94
|
+
# BidiBuffer is a thread-safe Buffer which can receive data from any thread.
|
|
86
95
|
#
|
|
87
|
-
# It
|
|
88
|
-
#
|
|
96
|
+
# It uses a dual-buffer strategy with mutex protection:
|
|
97
|
+
# - +@buffer+ is the main buffer, protected by +@buffer_mutex+
|
|
98
|
+
# - +@oob_buffer+ receives data when +@buffer_mutex+ is contended
|
|
99
|
+
#
|
|
100
|
+
# This allows non-blocking writes from any thread while maintaining thread safety.
|
|
89
101
|
class BidiBuffer < Buffer
|
|
90
102
|
def initialize(*)
|
|
91
103
|
super
|
|
92
|
-
@
|
|
104
|
+
@buffer_mutex = Thread::Mutex.new
|
|
93
105
|
@oob_mutex = Thread::Mutex.new
|
|
94
106
|
@oob_buffer = "".b
|
|
95
107
|
end
|
|
96
108
|
|
|
97
|
-
# buffers the +chunk+ to be sent
|
|
109
|
+
# buffers the +chunk+ to be sent (thread-safe, non-blocking)
|
|
98
110
|
def <<(chunk)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
if @buffer_mutex.try_lock
|
|
112
|
+
begin
|
|
113
|
+
super
|
|
114
|
+
ensure
|
|
115
|
+
@buffer_mutex.unlock
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
# another thread holds the lock, use OOB buffer to avoid blocking
|
|
119
|
+
@oob_mutex.synchronize { @oob_buffer << chunk }
|
|
120
|
+
end
|
|
102
121
|
end
|
|
103
122
|
|
|
104
|
-
# reconciles the main and secondary buffer (
|
|
123
|
+
# reconciles the main and secondary buffer (thread-safe, callable from any thread).
|
|
105
124
|
def rebuffer
|
|
106
|
-
|
|
125
|
+
@buffer_mutex.synchronize do
|
|
126
|
+
@oob_mutex.synchronize do
|
|
127
|
+
return if @oob_buffer.empty?
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
@buffer << @oob_buffer
|
|
130
|
+
@oob_buffer.clear
|
|
131
|
+
end
|
|
111
132
|
end
|
|
112
133
|
end
|
|
134
|
+
|
|
135
|
+
Buffer.instance_methods - Object.instance_methods - %i[<<].each do |meth|
|
|
136
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
|
137
|
+
def #{meth}(*) # def empty?
|
|
138
|
+
@buffer_mutex.synchronize { super }
|
|
139
|
+
end
|
|
140
|
+
MOD
|
|
141
|
+
end
|
|
113
142
|
end
|
|
114
143
|
|
|
115
144
|
# Proxy to wake up the session main loop when one
|
|
@@ -156,7 +185,13 @@ module HTTPX
|
|
|
156
185
|
|
|
157
186
|
def timeout; end
|
|
158
187
|
|
|
188
|
+
def inflight?
|
|
189
|
+
!@closed
|
|
190
|
+
end
|
|
191
|
+
|
|
159
192
|
def terminate
|
|
193
|
+
return if @closed
|
|
194
|
+
|
|
160
195
|
@pipe_write.close
|
|
161
196
|
@pipe_read.close
|
|
162
197
|
@closed = true
|
|
@@ -190,16 +225,20 @@ module HTTPX
|
|
|
190
225
|
def close(selector = Selector.new)
|
|
191
226
|
@signal.terminate
|
|
192
227
|
selector.deregister(@signal)
|
|
193
|
-
super
|
|
228
|
+
super
|
|
194
229
|
end
|
|
195
230
|
|
|
196
231
|
def select_connection(connection, selector)
|
|
232
|
+
return super unless connection.options.stream
|
|
233
|
+
|
|
197
234
|
super
|
|
198
235
|
selector.register(@signal)
|
|
199
236
|
connection.signal = @signal
|
|
200
237
|
end
|
|
201
238
|
|
|
202
239
|
def deselect_connection(connection, *)
|
|
240
|
+
return super unless connection.options.stream
|
|
241
|
+
|
|
203
242
|
super
|
|
204
243
|
|
|
205
244
|
connection.signal = nil
|
|
@@ -219,10 +258,14 @@ module HTTPX
|
|
|
219
258
|
end
|
|
220
259
|
|
|
221
260
|
def closed?
|
|
261
|
+
return super unless @options.stream
|
|
262
|
+
|
|
222
263
|
@closed
|
|
223
264
|
end
|
|
224
265
|
|
|
225
266
|
def can_buffer?
|
|
267
|
+
return super unless @options.stream
|
|
268
|
+
|
|
226
269
|
super && @state != :waiting_for_chunk
|
|
227
270
|
end
|
|
228
271
|
|
|
@@ -230,9 +273,13 @@ module HTTPX
|
|
|
230
273
|
# +:waiting_for_chunk+ state, which the request transitions to once payload
|
|
231
274
|
# is buffered.
|
|
232
275
|
def transition(nextstate)
|
|
276
|
+
return super unless @options.stream
|
|
277
|
+
|
|
233
278
|
headers_sent = @headers_sent
|
|
234
279
|
|
|
235
280
|
case nextstate
|
|
281
|
+
when :idle
|
|
282
|
+
headers_sent = false
|
|
236
283
|
when :waiting_for_chunk
|
|
237
284
|
return unless @state == :body
|
|
238
285
|
when :body
|
|
@@ -264,6 +311,8 @@ module HTTPX
|
|
|
264
311
|
end
|
|
265
312
|
|
|
266
313
|
def close
|
|
314
|
+
return super unless @options.stream
|
|
315
|
+
|
|
267
316
|
@mutex.synchronize do
|
|
268
317
|
return if @closed
|
|
269
318
|
|
|
@@ -278,10 +327,22 @@ module HTTPX
|
|
|
278
327
|
module RequestBodyMethods
|
|
279
328
|
def initialize(*, **)
|
|
280
329
|
super
|
|
330
|
+
|
|
331
|
+
return unless @options.stream
|
|
332
|
+
|
|
281
333
|
@headers.delete("content-length")
|
|
334
|
+
|
|
335
|
+
return unless @body
|
|
336
|
+
|
|
337
|
+
return if @body.is_a?(Transcoder::Body::Encoder)
|
|
338
|
+
|
|
339
|
+
raise Error, "bidirectional streams only allow the usage of the `:body` param to set request bodies." \
|
|
340
|
+
"You must encode it yourself if you wish to do so."
|
|
282
341
|
end
|
|
283
342
|
|
|
284
343
|
def empty?
|
|
344
|
+
return super unless @options.stream
|
|
345
|
+
|
|
285
346
|
false
|
|
286
347
|
end
|
|
287
348
|
end
|
|
@@ -293,18 +354,23 @@ module HTTPX
|
|
|
293
354
|
|
|
294
355
|
def initialize(*)
|
|
295
356
|
super
|
|
357
|
+
|
|
358
|
+
return unless @options.stream
|
|
359
|
+
|
|
296
360
|
@write_buffer = BidiBuffer.new(@options.buffer_size)
|
|
297
361
|
end
|
|
298
362
|
|
|
299
363
|
# rebuffers the +@write_buffer+ before calculating interests.
|
|
300
364
|
def interests
|
|
365
|
+
return super unless @options.stream
|
|
366
|
+
|
|
301
367
|
@write_buffer.rebuffer
|
|
302
368
|
|
|
303
369
|
super
|
|
304
370
|
end
|
|
305
371
|
|
|
306
372
|
def call
|
|
307
|
-
return super unless (error = @signal.error)
|
|
373
|
+
return super unless @options.stream && (error = @signal.error)
|
|
308
374
|
|
|
309
375
|
on_error(error)
|
|
310
376
|
end
|
|
@@ -312,6 +378,8 @@ module HTTPX
|
|
|
312
378
|
private
|
|
313
379
|
|
|
314
380
|
def set_parser_callbacks(parser)
|
|
381
|
+
return super unless @options.stream
|
|
382
|
+
|
|
315
383
|
super
|
|
316
384
|
parser.on(:flush_buffer) do
|
|
317
385
|
@signal.wakeup if @signal
|
data/lib/httpx/pool.rb
CHANGED
|
@@ -122,6 +122,12 @@ module HTTPX
|
|
|
122
122
|
|
|
123
123
|
@max_connections_cond.signal
|
|
124
124
|
@origin_conds[connection.origin.to_s].signal
|
|
125
|
+
|
|
126
|
+
# Observed situations where a session handling multiple requests in a loop
|
|
127
|
+
# across multiple threads checks the same connection in and out, while another
|
|
128
|
+
# thread which is waiting on the same connection never gets the chance to pick
|
|
129
|
+
# it up, because ruby's thread scheduler never switched on to it in the process.
|
|
130
|
+
Thread.pass
|
|
125
131
|
end
|
|
126
132
|
end
|
|
127
133
|
|
|
@@ -142,7 +148,6 @@ module HTTPX
|
|
|
142
148
|
|
|
143
149
|
def checkout_resolver(options)
|
|
144
150
|
resolver_type = options.resolver_class
|
|
145
|
-
resolver_type = Resolver.resolver_for(resolver_type, options)
|
|
146
151
|
|
|
147
152
|
@resolver_mtx.synchronize do
|
|
148
153
|
resolvers = @resolvers[resolver_type]
|
|
@@ -193,15 +198,19 @@ module HTTPX
|
|
|
193
198
|
end
|
|
194
199
|
|
|
195
200
|
def checkout_new_connection(uri, options)
|
|
196
|
-
options.connection_class.new(uri, options)
|
|
201
|
+
connection = options.connection_class.new(uri, options)
|
|
202
|
+
connection.log(level: 2) { "created connection##{connection.object_id} in pool##{object_id}" }
|
|
203
|
+
connection
|
|
197
204
|
end
|
|
198
205
|
|
|
199
206
|
def checkout_new_resolver(resolver_type, options)
|
|
200
|
-
if resolver_type.multi?
|
|
207
|
+
resolver = if resolver_type.multi?
|
|
201
208
|
Resolver::Multi.new(resolver_type, options)
|
|
202
209
|
else
|
|
203
210
|
resolver_type.new(options)
|
|
204
211
|
end
|
|
212
|
+
resolver.log(level: 2) { "created resolver##{resolver.object_id} in pool##{object_id}" }
|
|
213
|
+
resolver
|
|
205
214
|
end
|
|
206
215
|
|
|
207
216
|
# drops and returns the +connection+ from the connection pool; if +connection+ is <tt>nil</tt> (default),
|
data/lib/httpx/request/body.rb
CHANGED
|
@@ -128,7 +128,7 @@ module HTTPX
|
|
|
128
128
|
Transcoder::Body.encode(body)
|
|
129
129
|
elsif (form = params.delete(:form))
|
|
130
130
|
if Transcoder::Multipart.multipart?(form)
|
|
131
|
-
# @type var form: Transcoder::multipart_input
|
|
131
|
+
# @type var form: Transcoder::Multipart::multipart_input
|
|
132
132
|
Transcoder::Multipart.encode(form)
|
|
133
133
|
else
|
|
134
134
|
# @type var form: Transcoder::urlencoded_input
|
data/lib/httpx/request.rb
CHANGED
|
@@ -10,6 +10,7 @@ module HTTPX
|
|
|
10
10
|
extend Forwardable
|
|
11
11
|
include Loggable
|
|
12
12
|
include Callbacks
|
|
13
|
+
|
|
13
14
|
using URIExtensions
|
|
14
15
|
|
|
15
16
|
ALLOWED_URI_SCHEMES = %w[https http].freeze
|
|
@@ -90,12 +91,20 @@ module HTTPX
|
|
|
90
91
|
raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
|
|
91
92
|
|
|
92
93
|
@state = :idle
|
|
93
|
-
@response = @peer_address = @
|
|
94
|
+
@response = @peer_address = @informational_status = nil
|
|
94
95
|
@ping = false
|
|
95
96
|
@persistent = @options.persistent
|
|
96
97
|
@active_timeouts = []
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# dupped initialization
|
|
101
|
+
def initialize_dup(orig)
|
|
102
|
+
super
|
|
103
|
+
@uri = orig.instance_variable_get(:@uri).dup
|
|
104
|
+
@headers = orig.instance_variable_get(:@headers).dup
|
|
105
|
+
@body = orig.instance_variable_get(:@body).dup
|
|
106
|
+
end
|
|
107
|
+
|
|
99
108
|
def complete!(response = @response)
|
|
100
109
|
emit(:complete, response)
|
|
101
110
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "resolv"
|
|
4
|
+
|
|
5
|
+
module HTTPX
|
|
6
|
+
module Resolver::Cache
|
|
7
|
+
# Base class of the Resolver Cache adapter implementations.
|
|
8
|
+
#
|
|
9
|
+
# While resolver caches are not required to inherit from this class, it nevertheless provides
|
|
10
|
+
# common useful functions for desired functionality, such as singleton object ractor-safe access,
|
|
11
|
+
# or a default #resolve implementation which deals with IPs and the system hosts file.
|
|
12
|
+
#
|
|
13
|
+
class Base
|
|
14
|
+
MAX_CACHE_SIZE = 512
|
|
15
|
+
CACHE_MUTEX = Thread::Mutex.new
|
|
16
|
+
HOSTS = Resolv::Hosts.new
|
|
17
|
+
@cache = nil
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_reader :hosts_resolver
|
|
21
|
+
|
|
22
|
+
# returns the singleton instance to be used within the current ractor.
|
|
23
|
+
def cache(label)
|
|
24
|
+
return Ractor.store_if_absent(:"httpx_resolver_cache_#{label}") { new } if Utils.in_ractor?
|
|
25
|
+
|
|
26
|
+
@cache ||= CACHE_MUTEX.synchronize do
|
|
27
|
+
@cache || new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# resolves +hostname+ into an instance of HTTPX::Resolver::Entry if +hostname+ is an IP,
|
|
33
|
+
# or can be found in the cache, or can be found in the system hosts file.
|
|
34
|
+
def resolve(hostname)
|
|
35
|
+
ip_resolve(hostname) || get(hostname) || hosts_resolve(hostname)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
|
|
41
|
+
def ip_resolve(hostname)
|
|
42
|
+
[Resolver::Entry.new(hostname)]
|
|
43
|
+
rescue ArgumentError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
|
|
47
|
+
# found, or there is no hosts file.
|
|
48
|
+
def hosts_resolve(hostname)
|
|
49
|
+
ips = if Utils.in_ractor?
|
|
50
|
+
Ractor.store_if_absent(:httpx_hosts_resolver) { Resolv::Hosts.new }
|
|
51
|
+
else
|
|
52
|
+
HOSTS
|
|
53
|
+
end.getaddresses(hostname)
|
|
54
|
+
|
|
55
|
+
return if ips.empty?
|
|
56
|
+
|
|
57
|
+
ips.map { |ip| Resolver::Entry.new(ip) }
|
|
58
|
+
rescue IOError
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# not to be used directly!
|
|
62
|
+
def _get(hostname, lookups, hostnames, ttl)
|
|
63
|
+
return unless lookups.key?(hostname)
|
|
64
|
+
|
|
65
|
+
entries = lookups[hostname]
|
|
66
|
+
|
|
67
|
+
return unless entries
|
|
68
|
+
|
|
69
|
+
entries.delete_if do |address|
|
|
70
|
+
address["TTL"] < ttl
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if entries.empty?
|
|
74
|
+
lookups.delete(hostname)
|
|
75
|
+
hostnames.delete(hostname)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ips = entries.flat_map do |address|
|
|
79
|
+
if (als = address["alias"])
|
|
80
|
+
_get(als, lookups, hostnames, ttl)
|
|
81
|
+
else
|
|
82
|
+
Resolver::Entry.new(address["data"], address["TTL"])
|
|
83
|
+
end
|
|
84
|
+
end.compact
|
|
85
|
+
|
|
86
|
+
ips unless ips.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def _set(hostname, family, entries, lookups, hostnames)
|
|
90
|
+
# lru cleanup
|
|
91
|
+
while lookups.size >= MAX_CACHE_SIZE
|
|
92
|
+
hs = hostnames.shift
|
|
93
|
+
lookups.delete(hs)
|
|
94
|
+
end
|
|
95
|
+
hostnames << hostname
|
|
96
|
+
|
|
97
|
+
lookups[hostname] ||= [] # when there's no default proc
|
|
98
|
+
|
|
99
|
+
case family
|
|
100
|
+
when Socket::AF_INET6
|
|
101
|
+
lookups[hostname].concat(entries)
|
|
102
|
+
when Socket::AF_INET
|
|
103
|
+
lookups[hostname].unshift(*entries)
|
|
104
|
+
end
|
|
105
|
+
entries.each do |entry|
|
|
106
|
+
name = entry["name"]
|
|
107
|
+
next unless name != hostname
|
|
108
|
+
|
|
109
|
+
lookups[name] ||= []
|
|
110
|
+
|
|
111
|
+
case family
|
|
112
|
+
when Socket::AF_INET6
|
|
113
|
+
lookups[name] << entry
|
|
114
|
+
when Socket::AF_INET
|
|
115
|
+
lookups[name].unshift(entry)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def _evict(hostname, ip, lookups, hostnames)
|
|
121
|
+
return unless lookups.key?(hostname)
|
|
122
|
+
|
|
123
|
+
entries = lookups[hostname]
|
|
124
|
+
|
|
125
|
+
return unless entries
|
|
126
|
+
|
|
127
|
+
entries.delete_if { |entry| entry["data"] == ip }
|
|
128
|
+
|
|
129
|
+
return unless entries.empty?
|
|
130
|
+
|
|
131
|
+
lookups.delete(hostname)
|
|
132
|
+
hostnames.delete(hostname)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Resolver::Cache
|
|
5
|
+
# Implementation of a thread-safe in-memory LRU resolver cache.
|
|
6
|
+
class Memory < Base
|
|
7
|
+
def initialize
|
|
8
|
+
super
|
|
9
|
+
@hostnames = []
|
|
10
|
+
@lookups = Hash.new { |h, k| h[k] = [] }
|
|
11
|
+
@lookup_mutex = Thread::Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(hostname)
|
|
15
|
+
now = Utils.now
|
|
16
|
+
synchronize do |lookups, hostnames|
|
|
17
|
+
_get(hostname, lookups, hostnames, now)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set(hostname, family, entries)
|
|
22
|
+
synchronize do |lookups, hostnames|
|
|
23
|
+
_set(hostname, family, entries, lookups, hostnames)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def evict(hostname, ip)
|
|
28
|
+
ip = ip.to_s
|
|
29
|
+
|
|
30
|
+
synchronize do |lookups, hostnames|
|
|
31
|
+
_evict(hostname, ip, lookups, hostnames)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def synchronize
|
|
38
|
+
@lookup_mutex.synchronize { yield(@lookups, @hostnames) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpx/resolver/cache/base"
|
|
4
|
+
require "httpx/resolver/cache/memory"
|
|
5
|
+
|
|
6
|
+
module HTTPX::Resolver
|
|
7
|
+
# The internal resolvers cache adapters are defined under this namespace.
|
|
8
|
+
#
|
|
9
|
+
# Adapters must comply with the Resolver Cache Adapter API and implement the following methods:
|
|
10
|
+
#
|
|
11
|
+
# * #resolve: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache or system)
|
|
12
|
+
# * #get: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache)
|
|
13
|
+
# * #set: (String hostname, Integer ip_family, Array[dns_result]) -> void => stores the set of results in the cache indexes for
|
|
14
|
+
# the hostname and the IP family
|
|
15
|
+
# * #evict: (String hostname, _ToS ip) -> void => evicts the ip for the hostname from the cache (usually done when no longer reachable)
|
|
16
|
+
module Cache
|
|
17
|
+
end
|
|
18
|
+
end
|