httpx 1.4.4 → 1.5.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/doc/release_notes/1_5_0.md +126 -0
- data/lib/httpx/adapters/datadog.rb +24 -3
- data/lib/httpx/adapters/webmock.rb +1 -0
- data/lib/httpx/buffer.rb +16 -5
- data/lib/httpx/connection/http1.rb +8 -9
- data/lib/httpx/connection/http2.rb +48 -24
- data/lib/httpx/connection.rb +36 -19
- data/lib/httpx/errors.rb +2 -11
- data/lib/httpx/headers.rb +24 -23
- data/lib/httpx/io/ssl.rb +2 -1
- data/lib/httpx/io/tcp.rb +9 -7
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/loggable.rb +13 -1
- data/lib/httpx/options.rb +63 -48
- data/lib/httpx/parser/http1.rb +1 -1
- data/lib/httpx/plugins/aws_sigv4.rb +1 -0
- data/lib/httpx/plugins/callbacks.rb +19 -6
- data/lib/httpx/plugins/circuit_breaker.rb +4 -3
- data/lib/httpx/plugins/cookies/jar.rb +0 -2
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +7 -4
- data/lib/httpx/plugins/cookies.rb +4 -4
- data/lib/httpx/plugins/follow_redirects.rb +4 -2
- data/lib/httpx/plugins/grpc/call.rb +1 -1
- data/lib/httpx/plugins/h2c.rb +7 -1
- data/lib/httpx/plugins/persistent.rb +22 -1
- data/lib/httpx/plugins/proxy/http.rb +3 -1
- data/lib/httpx/plugins/query.rb +35 -0
- data/lib/httpx/plugins/response_cache/file_store.rb +115 -15
- data/lib/httpx/plugins/response_cache/store.rb +7 -67
- data/lib/httpx/plugins/response_cache.rb +179 -29
- data/lib/httpx/plugins/retries.rb +26 -14
- data/lib/httpx/plugins/stream.rb +4 -2
- data/lib/httpx/plugins/stream_bidi.rb +315 -0
- data/lib/httpx/pool.rb +58 -5
- data/lib/httpx/request/body.rb +1 -1
- data/lib/httpx/request.rb +6 -2
- data/lib/httpx/resolver/https.rb +10 -4
- data/lib/httpx/resolver/native.rb +13 -13
- data/lib/httpx/resolver/resolver.rb +4 -0
- data/lib/httpx/resolver/system.rb +37 -14
- data/lib/httpx/resolver.rb +2 -2
- data/lib/httpx/response/body.rb +10 -21
- data/lib/httpx/response/buffer.rb +36 -12
- data/lib/httpx/response.rb +11 -1
- data/lib/httpx/selector.rb +16 -12
- data/lib/httpx/session.rb +79 -19
- data/lib/httpx/timers.rb +24 -16
- data/lib/httpx/transcoder/multipart/decoder.rb +4 -2
- data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +1 -1
- data/sig/chainable.rbs +5 -2
- data/sig/connection/http2.rbs +11 -2
- data/sig/connection.rbs +4 -4
- data/sig/errors.rbs +0 -3
- data/sig/headers.rbs +15 -10
- data/sig/httpx.rbs +5 -1
- data/sig/io/tcp.rbs +6 -0
- data/sig/loggable.rbs +2 -0
- data/sig/options.rbs +7 -1
- data/sig/plugins/cookies/cookie.rbs +1 -3
- data/sig/plugins/cookies/jar.rbs +4 -4
- data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/h2c.rbs +4 -0
- data/sig/plugins/proxy/http.rbs +3 -0
- data/sig/plugins/proxy.rbs +4 -0
- data/sig/plugins/query.rbs +18 -0
- data/sig/plugins/response_cache/file_store.rbs +19 -0
- data/sig/plugins/response_cache/store.rbs +13 -0
- data/sig/plugins/response_cache.rbs +41 -19
- data/sig/plugins/retries.rbs +4 -3
- data/sig/plugins/stream.rbs +5 -1
- data/sig/plugins/stream_bidi.rbs +68 -0
- data/sig/plugins/upgrade/h2.rbs +9 -0
- data/sig/plugins/upgrade.rbs +5 -0
- data/sig/pool.rbs +5 -0
- data/sig/punycode.rbs +5 -0
- data/sig/request.rbs +2 -0
- data/sig/resolver/https.rbs +3 -2
- data/sig/resolver/native.rbs +1 -2
- data/sig/resolver/resolver.rbs +11 -3
- data/sig/resolver/system.rbs +19 -2
- data/sig/resolver.rbs +11 -7
- data/sig/response/body.rbs +3 -4
- data/sig/response/buffer.rbs +2 -3
- data/sig/response.rbs +2 -2
- data/sig/selector.rbs +20 -10
- data/sig/session.rbs +14 -6
- data/sig/timers.rbs +5 -7
- data/sig/transcoder/multipart.rbs +4 -3
- metadata +13 -2
data/lib/httpx/plugins/stream.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module HTTPX
|
4
4
|
class StreamResponse
|
5
|
+
attr_reader :request
|
6
|
+
|
5
7
|
def initialize(request, session)
|
6
8
|
@request = request
|
7
9
|
@options = @request.options
|
@@ -71,7 +73,7 @@ module HTTPX
|
|
71
73
|
|
72
74
|
# :nocov:
|
73
75
|
def inspect
|
74
|
-
"
|
76
|
+
"#<#{self.class}:#{object_id}>"
|
75
77
|
end
|
76
78
|
# :nocov:
|
77
79
|
|
@@ -114,7 +116,7 @@ module HTTPX
|
|
114
116
|
|
115
117
|
module Plugins
|
116
118
|
#
|
117
|
-
# This plugin adds support for
|
119
|
+
# This plugin adds support for streaming a response (useful for i.e. "text/event-stream" payloads).
|
118
120
|
#
|
119
121
|
# https://gitlab.com/os85/httpx/wikis/Stream
|
120
122
|
#
|
@@ -0,0 +1,315 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds support for bidirectional HTTP/2 streams.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/StreamBidi
|
9
|
+
#
|
10
|
+
# It is required that the request body allows chunk to be buffered, (i.e., responds to +#<<(chunk)+).
|
11
|
+
module StreamBidi
|
12
|
+
# Extension of the Connection::HTTP2 class, which adds functionality to
|
13
|
+
# deal with a request that can't be drained and must be interleaved with
|
14
|
+
# the response streams.
|
15
|
+
#
|
16
|
+
# The streams keeps send DATA frames while there's data; when they're ain't,
|
17
|
+
# the stream is kept open; it must be explicitly closed by the end user.
|
18
|
+
#
|
19
|
+
class HTTP2Bidi < Connection::HTTP2
|
20
|
+
def initialize(*)
|
21
|
+
super
|
22
|
+
@lock = Thread::Mutex.new
|
23
|
+
end
|
24
|
+
|
25
|
+
%i[close empty? exhausted? send <<].each do |lock_meth|
|
26
|
+
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
27
|
+
# lock.aware version of +#{lock_meth}+
|
28
|
+
def #{lock_meth}(*) # def close(*)
|
29
|
+
return super if @lock.owned?
|
30
|
+
|
31
|
+
# small race condition between
|
32
|
+
# checking for ownership and
|
33
|
+
# acquiring lock.
|
34
|
+
# TODO: fix this at the parser.
|
35
|
+
@lock.synchronize { super }
|
36
|
+
end
|
37
|
+
METH
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
%i[join_headers join_trailers join_body].each do |lock_meth|
|
43
|
+
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
44
|
+
# lock.aware version of +#{lock_meth}+
|
45
|
+
def #{lock_meth}(*) # def join_headers(*)
|
46
|
+
return super if @lock.owned?
|
47
|
+
|
48
|
+
# small race condition between
|
49
|
+
# checking for ownership and
|
50
|
+
# acquiring lock.
|
51
|
+
# TODO: fix this at the parser.
|
52
|
+
@lock.synchronize { super }
|
53
|
+
end
|
54
|
+
METH
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_stream(stream, request)
|
58
|
+
request.on(:body) do
|
59
|
+
next unless request.headers_sent
|
60
|
+
|
61
|
+
handle(request, stream)
|
62
|
+
|
63
|
+
emit(:flush_buffer)
|
64
|
+
end
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
# when there ain't more chunks, it makes the buffer as full.
|
69
|
+
def send_chunk(request, stream, chunk, next_chunk)
|
70
|
+
super
|
71
|
+
|
72
|
+
return if next_chunk
|
73
|
+
|
74
|
+
request.transition(:waiting_for_chunk)
|
75
|
+
throw(:buffer_full)
|
76
|
+
end
|
77
|
+
|
78
|
+
# sets end-stream flag when the request is closed.
|
79
|
+
def end_stream?(request, next_chunk)
|
80
|
+
request.closed? && next_chunk.nil?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# BidiBuffer is a Buffer which can be receive data from threads othr
|
85
|
+
# than the thread of the corresponding Connection/Session.
|
86
|
+
#
|
87
|
+
# It synchronizes access to a secondary internal +@oob_buffer+, which periodically
|
88
|
+
# is reconciled to the main internal +@buffer+.
|
89
|
+
class BidiBuffer < Buffer
|
90
|
+
def initialize(*)
|
91
|
+
super
|
92
|
+
@parent_thread = Thread.current
|
93
|
+
@oob_mutex = Thread::Mutex.new
|
94
|
+
@oob_buffer = "".b
|
95
|
+
end
|
96
|
+
|
97
|
+
# buffers the +chunk+ to be sent
|
98
|
+
def <<(chunk)
|
99
|
+
return super if Thread.current == @parent_thread
|
100
|
+
|
101
|
+
@oob_mutex.synchronize { @oob_buffer << chunk }
|
102
|
+
end
|
103
|
+
|
104
|
+
# reconciles the main and secondary buffer (which receives data from other threads).
|
105
|
+
def rebuffer
|
106
|
+
raise Error, "can only rebuffer while waiting on a response" unless Thread.current == @parent_thread
|
107
|
+
|
108
|
+
@oob_mutex.synchronize do
|
109
|
+
@buffer << @oob_buffer
|
110
|
+
@oob_buffer.clear
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Proxy to wake up the session main loop when one
|
116
|
+
# of the connections has buffered data to write. It abides by the HTTPX::_Selectable API,
|
117
|
+
# which allows it to be registered in the selector alongside actual HTTP-based
|
118
|
+
# HTTPX::Connection objects.
|
119
|
+
class Signal
|
120
|
+
def initialize
|
121
|
+
@closed = false
|
122
|
+
@pipe_read, @pipe_write = ::IO.pipe
|
123
|
+
end
|
124
|
+
|
125
|
+
def state
|
126
|
+
@closed ? :closed : :open
|
127
|
+
end
|
128
|
+
|
129
|
+
# noop
|
130
|
+
def log(**); end
|
131
|
+
|
132
|
+
def to_io
|
133
|
+
@pipe_read.to_io
|
134
|
+
end
|
135
|
+
|
136
|
+
def wakeup
|
137
|
+
return if @closed
|
138
|
+
|
139
|
+
@pipe_write.write("\0")
|
140
|
+
end
|
141
|
+
|
142
|
+
def call
|
143
|
+
return if @closed
|
144
|
+
|
145
|
+
@pipe_read.readpartial(1)
|
146
|
+
end
|
147
|
+
|
148
|
+
def interests
|
149
|
+
return if @closed
|
150
|
+
|
151
|
+
:r
|
152
|
+
end
|
153
|
+
|
154
|
+
def timeout; end
|
155
|
+
|
156
|
+
def terminate
|
157
|
+
@pipe_write.close
|
158
|
+
@pipe_read.close
|
159
|
+
@closed = true
|
160
|
+
end
|
161
|
+
|
162
|
+
# noop (the owner connection will take of it)
|
163
|
+
def handle_socket_timeout(interval); end
|
164
|
+
end
|
165
|
+
|
166
|
+
class << self
|
167
|
+
def load_dependencies(klass)
|
168
|
+
klass.plugin(:stream)
|
169
|
+
end
|
170
|
+
|
171
|
+
def extra_options(options)
|
172
|
+
options.merge(fallback_protocol: "h2")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
module InstanceMethods
|
177
|
+
def initialize(*)
|
178
|
+
@signal = Signal.new
|
179
|
+
super
|
180
|
+
end
|
181
|
+
|
182
|
+
def close(selector = Selector.new)
|
183
|
+
@signal.terminate
|
184
|
+
selector.deregister(@signal)
|
185
|
+
super(selector)
|
186
|
+
end
|
187
|
+
|
188
|
+
def select_connection(connection, selector)
|
189
|
+
super
|
190
|
+
selector.register(@signal)
|
191
|
+
connection.signal = @signal
|
192
|
+
end
|
193
|
+
|
194
|
+
def deselect_connection(connection, *)
|
195
|
+
super
|
196
|
+
connection.signal = nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Adds synchronization to request operations which may buffer payloads from different
|
201
|
+
# threads.
|
202
|
+
module RequestMethods
|
203
|
+
attr_accessor :headers_sent
|
204
|
+
|
205
|
+
def initialize(*)
|
206
|
+
super
|
207
|
+
@headers_sent = false
|
208
|
+
@closed = false
|
209
|
+
@mutex = Thread::Mutex.new
|
210
|
+
end
|
211
|
+
|
212
|
+
def closed?
|
213
|
+
@closed
|
214
|
+
end
|
215
|
+
|
216
|
+
def can_buffer?
|
217
|
+
super && @state != :waiting_for_chunk
|
218
|
+
end
|
219
|
+
|
220
|
+
# overrides state management transitions to introduce an intermediate
|
221
|
+
# +:waiting_for_chunk+ state, which the request transitions to once payload
|
222
|
+
# is buffered.
|
223
|
+
def transition(nextstate)
|
224
|
+
headers_sent = @headers_sent
|
225
|
+
|
226
|
+
case nextstate
|
227
|
+
when :waiting_for_chunk
|
228
|
+
return unless @state == :body
|
229
|
+
when :body
|
230
|
+
case @state
|
231
|
+
when :headers
|
232
|
+
headers_sent = true
|
233
|
+
when :waiting_for_chunk
|
234
|
+
# HACK: to allow super to pass through
|
235
|
+
@state = :headers
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
super.tap do
|
240
|
+
# delay setting this up until after the first transition to :body
|
241
|
+
@headers_sent = headers_sent
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def <<(chunk)
|
246
|
+
@mutex.synchronize do
|
247
|
+
if @drainer
|
248
|
+
@body.clear if @body.respond_to?(:clear)
|
249
|
+
@drainer = nil
|
250
|
+
end
|
251
|
+
@body << chunk
|
252
|
+
|
253
|
+
transition(:body)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def close
|
258
|
+
@mutex.synchronize do
|
259
|
+
return if @closed
|
260
|
+
|
261
|
+
@closed = true
|
262
|
+
end
|
263
|
+
|
264
|
+
# last chunk to send which ends the stream
|
265
|
+
self << ""
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
module RequestBodyMethods
|
270
|
+
def initialize(*, **)
|
271
|
+
super
|
272
|
+
@headers.delete("content-length")
|
273
|
+
end
|
274
|
+
|
275
|
+
def empty?
|
276
|
+
false
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# overrides the declaration of +@write_buffer+, which is now a thread-safe buffer
|
281
|
+
# responding to the same API.
|
282
|
+
module ConnectionMethods
|
283
|
+
attr_writer :signal
|
284
|
+
|
285
|
+
def initialize(*)
|
286
|
+
super
|
287
|
+
@write_buffer = BidiBuffer.new(@options.buffer_size)
|
288
|
+
end
|
289
|
+
|
290
|
+
# rebuffers the +@write_buffer+ before calculating interests.
|
291
|
+
def interests
|
292
|
+
@write_buffer.rebuffer
|
293
|
+
|
294
|
+
super
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def parser_type(protocol)
|
300
|
+
return HTTP2Bidi if protocol == "h2"
|
301
|
+
|
302
|
+
super
|
303
|
+
end
|
304
|
+
|
305
|
+
def set_parser_callbacks(parser)
|
306
|
+
super
|
307
|
+
parser.on(:flush_buffer) do
|
308
|
+
@signal.wakeup if @signal
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
register_plugin :stream_bidi, StreamBidi
|
314
|
+
end
|
315
|
+
end
|
data/lib/httpx/pool.rb
CHANGED
@@ -13,25 +13,28 @@ module HTTPX
|
|
13
13
|
|
14
14
|
# Sets up the connection pool with the given +options+, which can be the following:
|
15
15
|
#
|
16
|
+
# :max_connections:: the maximum number of connections held in the pool.
|
16
17
|
# :max_connections_per_origin :: the maximum number of connections held in the pool pointing to a given origin.
|
17
18
|
# :pool_timeout :: the number of seconds to wait for a connection to a given origin (before raising HTTPX::PoolTimeoutError)
|
18
19
|
#
|
19
20
|
def initialize(options)
|
21
|
+
@max_connections = options.fetch(:max_connections, Float::INFINITY)
|
20
22
|
@max_connections_per_origin = options.fetch(:max_connections_per_origin, Float::INFINITY)
|
21
23
|
@pool_timeout = options.fetch(:pool_timeout, POOL_TIMEOUT)
|
22
24
|
@resolvers = Hash.new { |hs, resolver_type| hs[resolver_type] = [] }
|
23
25
|
@resolver_mtx = Thread::Mutex.new
|
24
26
|
@connections = []
|
25
27
|
@connection_mtx = Thread::Mutex.new
|
28
|
+
@connections_counter = 0
|
29
|
+
@max_connections_cond = ConditionVariable.new
|
26
30
|
@origin_counters = Hash.new(0)
|
27
31
|
@origin_conds = Hash.new { |hs, orig| hs[orig] = ConditionVariable.new }
|
28
32
|
end
|
29
33
|
|
34
|
+
# connections returned by this function are not expected to return to the connection pool.
|
30
35
|
def pop_connection
|
31
36
|
@connection_mtx.synchronize do
|
32
|
-
|
33
|
-
@origin_conds.delete(conn.origin) if conn && (@origin_counters[conn.origin.to_s] -= 1).zero?
|
34
|
-
conn
|
37
|
+
drop_connection
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
@@ -44,13 +47,34 @@ module HTTPX
|
|
44
47
|
|
45
48
|
@connection_mtx.synchronize do
|
46
49
|
acquire_connection(uri, options) || begin
|
50
|
+
if @connections_counter == @max_connections
|
51
|
+
# this takes precedence over per-origin
|
52
|
+
@max_connections_cond.wait(@connection_mtx, @pool_timeout)
|
53
|
+
|
54
|
+
acquire_connection(uri, options) || begin
|
55
|
+
if @connections_counter == @max_connections
|
56
|
+
# if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
|
57
|
+
# this means that all of them are persistent or being used, so raise a timeout error.
|
58
|
+
conn = @connections.find { |c| c.state == :closed }
|
59
|
+
|
60
|
+
raise PoolTimeoutError.new(@pool_timeout,
|
61
|
+
"Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
|
62
|
+
|
63
|
+
drop_connection(conn)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
47
68
|
if @origin_counters[uri.origin] == @max_connections_per_origin
|
48
69
|
|
49
70
|
@origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
|
50
71
|
|
51
|
-
return acquire_connection(uri, options) ||
|
72
|
+
return acquire_connection(uri, options) ||
|
73
|
+
raise(PoolTimeoutError.new(@pool_timeout,
|
74
|
+
"Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
|
52
75
|
end
|
53
76
|
|
77
|
+
@connections_counter += 1
|
54
78
|
@origin_counters[uri.origin] += 1
|
55
79
|
|
56
80
|
checkout_new_connection(uri, options)
|
@@ -64,6 +88,7 @@ module HTTPX
|
|
64
88
|
@connection_mtx.synchronize do
|
65
89
|
@connections << connection
|
66
90
|
|
91
|
+
@max_connections_cond.signal
|
67
92
|
@origin_conds[connection.origin.to_s].signal
|
68
93
|
end
|
69
94
|
end
|
@@ -107,6 +132,15 @@ module HTTPX
|
|
107
132
|
end
|
108
133
|
end
|
109
134
|
|
135
|
+
# :nocov:
|
136
|
+
def inspect
|
137
|
+
"#<#{self.class}:#{object_id} " \
|
138
|
+
"@max_connections_per_origin=#{@max_connections_per_origin} " \
|
139
|
+
"@pool_timeout=#{@pool_timeout} " \
|
140
|
+
"@connections=#{@connections.size}>"
|
141
|
+
end
|
142
|
+
# :nocov:
|
143
|
+
|
110
144
|
private
|
111
145
|
|
112
146
|
def acquire_connection(uri, options)
|
@@ -114,7 +148,9 @@ module HTTPX
|
|
114
148
|
connection.match?(uri, options)
|
115
149
|
end
|
116
150
|
|
117
|
-
|
151
|
+
return unless idx
|
152
|
+
|
153
|
+
@connections.delete_at(idx)
|
118
154
|
end
|
119
155
|
|
120
156
|
def checkout_new_connection(uri, options)
|
@@ -128,5 +164,22 @@ module HTTPX
|
|
128
164
|
resolver_type.new(options)
|
129
165
|
end
|
130
166
|
end
|
167
|
+
|
168
|
+
# drops and returns the +connection+ from the connection pool; if +connection+ is <tt>nil</tt> (default),
|
169
|
+
# the first available connection from the pool will be dropped.
|
170
|
+
def drop_connection(connection = nil)
|
171
|
+
if connection
|
172
|
+
@connections.delete(connection)
|
173
|
+
else
|
174
|
+
connection = @connections.shift
|
175
|
+
|
176
|
+
return unless connection
|
177
|
+
end
|
178
|
+
|
179
|
+
@connections_counter -= 1
|
180
|
+
@origin_conds.delete(connection.origin) if (@origin_counters[connection.origin.to_s] -= 1).zero?
|
181
|
+
|
182
|
+
connection
|
183
|
+
end
|
131
184
|
end
|
132
185
|
end
|
data/lib/httpx/request/body.rb
CHANGED
data/lib/httpx/request.rb
CHANGED
@@ -155,6 +155,10 @@ module HTTPX
|
|
155
155
|
:w
|
156
156
|
end
|
157
157
|
|
158
|
+
def can_buffer?
|
159
|
+
@state != :done
|
160
|
+
end
|
161
|
+
|
158
162
|
# merges +h+ into the instance of HTTPX::Headers of the request.
|
159
163
|
def merge_headers(h)
|
160
164
|
@headers = @headers.merge(h)
|
@@ -222,7 +226,7 @@ module HTTPX
|
|
222
226
|
return @query if defined?(@query)
|
223
227
|
|
224
228
|
query = []
|
225
|
-
if (q = @query_params)
|
229
|
+
if (q = @query_params) && !q.empty?
|
226
230
|
query << Transcoder::Form.encode(q)
|
227
231
|
end
|
228
232
|
query << @uri.query if @uri.query
|
@@ -247,7 +251,7 @@ module HTTPX
|
|
247
251
|
|
248
252
|
# :nocov:
|
249
253
|
def inspect
|
250
|
-
"
|
254
|
+
"#<#{self.class}:#{object_id} " \
|
251
255
|
"#{@verb} " \
|
252
256
|
"#{uri} " \
|
253
257
|
"@headers=#{@headers} " \
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -2,11 +2,14 @@
|
|
2
2
|
|
3
3
|
require "resolv"
|
4
4
|
require "uri"
|
5
|
-
require "cgi"
|
6
5
|
require "forwardable"
|
7
6
|
require "httpx/base64"
|
8
7
|
|
9
8
|
module HTTPX
|
9
|
+
# Implementation of a DoH name resolver (https://www.youtube.com/watch?v=unMXvnY2FNM).
|
10
|
+
# It wraps an HTTPX::Connection object which integrates with the main session in the
|
11
|
+
# same manner as other performed HTTP requests.
|
12
|
+
#
|
10
13
|
class Resolver::HTTPS < Resolver::Resolver
|
11
14
|
extend Forwardable
|
12
15
|
using URIExtensions
|
@@ -27,14 +30,13 @@ module HTTPX
|
|
27
30
|
use_get: false,
|
28
31
|
}.freeze
|
29
32
|
|
30
|
-
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight
|
33
|
+
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?, :handle_socket_timeout
|
31
34
|
|
32
35
|
def initialize(_, options)
|
33
36
|
super
|
34
37
|
@resolver_options = DEFAULTS.merge(@options.resolver_options)
|
35
38
|
@queries = {}
|
36
39
|
@requests = {}
|
37
|
-
@connections = []
|
38
40
|
@uri = URI(@resolver_options[:uri])
|
39
41
|
@uri_addresses = nil
|
40
42
|
@resolver = Resolv::DNS.new
|
@@ -75,7 +77,11 @@ module HTTPX
|
|
75
77
|
|
76
78
|
private
|
77
79
|
|
78
|
-
def resolve(connection =
|
80
|
+
def resolve(connection = nil, hostname = nil)
|
81
|
+
@connections.shift until @connections.empty? || @connections.first.state != :closed
|
82
|
+
|
83
|
+
connection ||= @connections.first
|
84
|
+
|
79
85
|
return unless connection
|
80
86
|
|
81
87
|
hostname ||= @queries.key(connection)
|
@@ -4,6 +4,9 @@ require "forwardable"
|
|
4
4
|
require "resolv"
|
5
5
|
|
6
6
|
module HTTPX
|
7
|
+
# Implements a pure ruby name resolver, which abides by the Selectable API.
|
8
|
+
# It delegates DNS payload encoding/decoding to the +resolv+ stlid gem.
|
9
|
+
#
|
7
10
|
class Resolver::Native < Resolver::Resolver
|
8
11
|
extend Forwardable
|
9
12
|
using URIExtensions
|
@@ -34,7 +37,6 @@ module HTTPX
|
|
34
37
|
@search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
|
35
38
|
@_timeouts = Array(@resolver_options[:timeouts])
|
36
39
|
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
37
|
-
@connections = []
|
38
40
|
@name = nil
|
39
41
|
@queries = {}
|
40
42
|
@read_buffer = "".b
|
@@ -46,6 +48,10 @@ module HTTPX
|
|
46
48
|
transition(:closed)
|
47
49
|
end
|
48
50
|
|
51
|
+
def terminate
|
52
|
+
emit(:close, self)
|
53
|
+
end
|
54
|
+
|
49
55
|
def closed?
|
50
56
|
@state == :closed
|
51
57
|
end
|
@@ -120,10 +126,7 @@ module HTTPX
|
|
120
126
|
@ns_index += 1
|
121
127
|
nameserver = @nameserver
|
122
128
|
if nameserver && @ns_index < nameserver.size
|
123
|
-
log
|
124
|
-
"resolver #{FAMILY_TYPES[@record_type]}: " \
|
125
|
-
"failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
|
126
|
-
end
|
129
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
|
127
130
|
transition(:idle)
|
128
131
|
@timeouts.clear
|
129
132
|
retry
|
@@ -158,9 +161,7 @@ module HTTPX
|
|
158
161
|
timeouts = @timeouts[h]
|
159
162
|
|
160
163
|
if !timeouts.empty?
|
161
|
-
log
|
162
|
-
"resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..."
|
163
|
-
end
|
164
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..." }
|
164
165
|
# must downgrade to tcp AND retry on same host as last
|
165
166
|
downgrade_socket
|
166
167
|
resolve(connection, h)
|
@@ -388,10 +389,9 @@ module HTTPX
|
|
388
389
|
|
389
390
|
if hostname.nil?
|
390
391
|
hostname = connection.peer.host
|
391
|
-
|
392
|
-
"resolver #{FAMILY_TYPES[@record_type]}: "
|
393
|
-
|
394
|
-
end if connection.peer.non_ascii_hostname
|
392
|
+
if connection.peer.non_ascii_hostname
|
393
|
+
log { "resolver #{FAMILY_TYPES[@record_type]}: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" }
|
394
|
+
end
|
395
395
|
|
396
396
|
hostname = generate_candidates(hostname).each do |name|
|
397
397
|
@queries[name] = connection
|
@@ -507,7 +507,7 @@ module HTTPX
|
|
507
507
|
end
|
508
508
|
|
509
509
|
while (connection = @connections.shift)
|
510
|
-
emit_resolve_error(connection, host, error)
|
510
|
+
emit_resolve_error(connection, connection.peer.host, error)
|
511
511
|
end
|
512
512
|
end
|
513
513
|
end
|
@@ -4,6 +4,9 @@ require "resolv"
|
|
4
4
|
require "ipaddr"
|
5
5
|
|
6
6
|
module HTTPX
|
7
|
+
# Base class for all internal internet name resolvers. It handles basic blocks
|
8
|
+
# from the Selectable API.
|
9
|
+
#
|
7
10
|
class Resolver::Resolver
|
8
11
|
include Callbacks
|
9
12
|
include Loggable
|
@@ -36,6 +39,7 @@ module HTTPX
|
|
36
39
|
@family = family
|
37
40
|
@record_type = RECORD_TYPES[family]
|
38
41
|
@options = options
|
42
|
+
@connections = []
|
39
43
|
|
40
44
|
set_resolver_callbacks
|
41
45
|
end
|