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
@@ -3,6 +3,15 @@
|
|
3
3
|
require "resolv"
|
4
4
|
|
5
5
|
module HTTPX
|
6
|
+
# Implementation of a synchronous name resolver which relies on the system resolver,
|
7
|
+
# which is lib'c getaddrinfo function (abstracted in ruby via Addrinfo.getaddrinfo).
|
8
|
+
#
|
9
|
+
# Its main advantage is relying on the reference implementation for name resolution
|
10
|
+
# across most/all OSs which deploy ruby (it's what TCPSocket also uses), its main
|
11
|
+
# disadvantage is the inability to set timeouts / check socket for readiness events,
|
12
|
+
# hence why it relies on using the Timeout module, which poses a lot of problems for
|
13
|
+
# the selector loop, specially when network is unstable.
|
14
|
+
#
|
6
15
|
class Resolver::System < Resolver::Resolver
|
7
16
|
using URIExtensions
|
8
17
|
|
@@ -23,14 +32,13 @@ module HTTPX
|
|
23
32
|
attr_reader :state
|
24
33
|
|
25
34
|
def initialize(options)
|
26
|
-
super(
|
35
|
+
super(0, options)
|
27
36
|
@resolver_options = @options.resolver_options
|
28
37
|
resolv_options = @resolver_options.dup
|
29
38
|
timeouts = resolv_options.delete(:timeouts) || Resolver::RESOLVE_TIMEOUT
|
30
39
|
@_timeouts = Array(timeouts)
|
31
40
|
@timeouts = Hash.new { |tims, host| tims[host] = @_timeouts.dup }
|
32
41
|
resolv_options.delete(:cache)
|
33
|
-
@connections = []
|
34
42
|
@queries = []
|
35
43
|
@ips = []
|
36
44
|
@pipe_mutex = Thread::Mutex.new
|
@@ -100,7 +108,14 @@ module HTTPX
|
|
100
108
|
def handle_socket_timeout(interval)
|
101
109
|
error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
|
102
110
|
error.set_backtrace(caller)
|
103
|
-
|
111
|
+
@queries.each do |host, connection|
|
112
|
+
@connections.delete(connection)
|
113
|
+
emit_resolve_error(connection, host, error)
|
114
|
+
end
|
115
|
+
|
116
|
+
while (connection = @connections.shift)
|
117
|
+
emit_resolve_error(connection, connection.peer.host, error)
|
118
|
+
end
|
104
119
|
end
|
105
120
|
|
106
121
|
private
|
@@ -131,19 +146,22 @@ module HTTPX
|
|
131
146
|
case event
|
132
147
|
when DONE
|
133
148
|
*pair, addrs = @pipe_mutex.synchronize { @ips.pop }
|
134
|
-
|
135
|
-
|
136
|
-
|
149
|
+
if pair
|
150
|
+
@queries.delete(pair)
|
151
|
+
family, connection = pair
|
152
|
+
@connections.delete(connection)
|
137
153
|
|
138
|
-
|
139
|
-
|
154
|
+
catch(:coalesced) { emit_addresses(connection, family, addrs) }
|
155
|
+
end
|
140
156
|
when ERROR
|
141
157
|
*pair, error = @pipe_mutex.synchronize { @ips.pop }
|
142
|
-
|
143
|
-
|
158
|
+
if pair && error
|
159
|
+
@queries.delete(pair)
|
160
|
+
@connections.delete(connection)
|
144
161
|
|
145
|
-
|
146
|
-
|
162
|
+
_, connection = pair
|
163
|
+
emit_resolve_error(connection, connection.peer.host, error)
|
164
|
+
end
|
147
165
|
end
|
148
166
|
end
|
149
167
|
|
@@ -152,11 +170,16 @@ module HTTPX
|
|
152
170
|
resolve
|
153
171
|
end
|
154
172
|
|
155
|
-
def resolve(connection =
|
173
|
+
def resolve(connection = nil, hostname = nil)
|
174
|
+
@connections.shift until @connections.empty? || @connections.first.state != :closed
|
175
|
+
|
176
|
+
connection ||= @connections.first
|
177
|
+
|
156
178
|
raise Error, "no URI to resolve" unless connection
|
179
|
+
|
157
180
|
return unless @queries.empty?
|
158
181
|
|
159
|
-
hostname
|
182
|
+
hostname ||= connection.peer.host
|
160
183
|
scheme = connection.origin.scheme
|
161
184
|
log do
|
162
185
|
"resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
|
data/lib/httpx/resolver.rb
CHANGED
data/lib/httpx/response/body.rb
CHANGED
@@ -11,6 +11,9 @@ module HTTPX
|
|
11
11
|
# Array of encodings contained in the response "content-encoding" header.
|
12
12
|
attr_reader :encodings
|
13
13
|
|
14
|
+
attr_reader :buffer
|
15
|
+
protected :buffer
|
16
|
+
|
14
17
|
# initialized with the corresponding HTTPX::Response +response+ and HTTPX::Options +options+.
|
15
18
|
def initialize(response, options)
|
16
19
|
@response = response
|
@@ -148,18 +151,17 @@ module HTTPX
|
|
148
151
|
end
|
149
152
|
|
150
153
|
def ==(other)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
end
|
154
|
+
super || case other
|
155
|
+
when Response::Body
|
156
|
+
@buffer == other.buffer
|
157
|
+
else
|
158
|
+
@buffer = other
|
159
|
+
end
|
158
160
|
end
|
159
161
|
|
160
162
|
# :nocov:
|
161
163
|
def inspect
|
162
|
-
"
|
164
|
+
"#<#{self.class}:#{object_id} " \
|
163
165
|
"@state=#{@state} " \
|
164
166
|
"@length=#{@length}>"
|
165
167
|
end
|
@@ -226,19 +228,6 @@ module HTTPX
|
|
226
228
|
@state = nextstate
|
227
229
|
end
|
228
230
|
|
229
|
-
def _with_same_buffer_pos # :nodoc:
|
230
|
-
return yield unless @buffer && @buffer.respond_to?(:pos)
|
231
|
-
|
232
|
-
# @type ivar @buffer: StringIO | Tempfile
|
233
|
-
current_pos = @buffer.pos
|
234
|
-
@buffer.rewind
|
235
|
-
begin
|
236
|
-
yield
|
237
|
-
ensure
|
238
|
-
@buffer.pos = current_pos
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
231
|
class << self
|
243
232
|
def initialize_inflater_by_encoding(encoding, response, **kwargs) # :nodoc:
|
244
233
|
case encoding
|
@@ -7,6 +7,9 @@ require "tempfile"
|
|
7
7
|
module HTTPX
|
8
8
|
# wraps and delegates to an internal buffer, which can be a StringIO or a Tempfile.
|
9
9
|
class Response::Buffer < SimpleDelegator
|
10
|
+
attr_reader :buffer
|
11
|
+
protected :buffer
|
12
|
+
|
10
13
|
# initializes buffer with the +threshold_size+ over which the payload gets buffer to a tempfile,
|
11
14
|
# the initial +bytesize+, and the +encoding+.
|
12
15
|
def initialize(threshold_size:, bytesize: 0, encoding: Encoding::BINARY)
|
@@ -20,7 +23,14 @@ module HTTPX
|
|
20
23
|
def initialize_dup(other)
|
21
24
|
super
|
22
25
|
|
23
|
-
|
26
|
+
# create new descriptor in READ-ONLY mode
|
27
|
+
@buffer =
|
28
|
+
case other.buffer
|
29
|
+
when StringIO
|
30
|
+
StringIO.new(other.buffer.string, mode: File::RDONLY)
|
31
|
+
else
|
32
|
+
other.buffer.class.new(other.buffer.path, encoding: Encoding::BINARY, mode: File::RDONLY)
|
33
|
+
end
|
24
34
|
end
|
25
35
|
|
26
36
|
# size in bytes of the buffered content.
|
@@ -46,7 +56,7 @@ module HTTPX
|
|
46
56
|
end
|
47
57
|
when Tempfile
|
48
58
|
rewind
|
49
|
-
content =
|
59
|
+
content = @buffer.read
|
50
60
|
begin
|
51
61
|
content.force_encoding(@encoding)
|
52
62
|
rescue ArgumentError # ex: unknown encoding name - utf
|
@@ -61,6 +71,30 @@ module HTTPX
|
|
61
71
|
@buffer.unlink if @buffer.respond_to?(:unlink)
|
62
72
|
end
|
63
73
|
|
74
|
+
def ==(other)
|
75
|
+
super || begin
|
76
|
+
return false unless other.is_a?(Response::Buffer)
|
77
|
+
|
78
|
+
if @buffer.nil?
|
79
|
+
other.buffer.nil?
|
80
|
+
elsif @buffer.respond_to?(:read) &&
|
81
|
+
other.respond_to?(:read)
|
82
|
+
buffer_pos = @buffer.pos
|
83
|
+
other_pos = other.buffer.pos
|
84
|
+
@buffer.rewind
|
85
|
+
other.buffer.rewind
|
86
|
+
begin
|
87
|
+
FileUtils.compare_stream(@buffer, other.buffer)
|
88
|
+
ensure
|
89
|
+
@buffer.pos = buffer_pos
|
90
|
+
other.buffer.pos = other_pos
|
91
|
+
end
|
92
|
+
else
|
93
|
+
to_s == other.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
64
98
|
private
|
65
99
|
|
66
100
|
# initializes the buffer into a StringIO, or turns it into a Tempfile when the threshold
|
@@ -82,15 +116,5 @@ module HTTPX
|
|
82
116
|
|
83
117
|
__setobj__(@buffer)
|
84
118
|
end
|
85
|
-
|
86
|
-
def _with_same_buffer_pos # :nodoc:
|
87
|
-
current_pos = @buffer.pos
|
88
|
-
@buffer.rewind
|
89
|
-
begin
|
90
|
-
yield
|
91
|
-
ensure
|
92
|
-
@buffer.pos = current_pos
|
93
|
-
end
|
94
|
-
end
|
95
119
|
end
|
96
120
|
end
|
data/lib/httpx/response.rb
CHANGED
@@ -71,6 +71,14 @@ module HTTPX
|
|
71
71
|
@content_type = nil
|
72
72
|
end
|
73
73
|
|
74
|
+
# dupped initialization
|
75
|
+
def initialize_dup(orig)
|
76
|
+
super
|
77
|
+
# if a response gets dupped, the body handle must also get dupped to prevent
|
78
|
+
# two responses from using the same file handle to read.
|
79
|
+
@body = orig.body.dup
|
80
|
+
end
|
81
|
+
|
74
82
|
# closes the respective +@request+ and +@body+.
|
75
83
|
def close
|
76
84
|
@request.close
|
@@ -126,7 +134,7 @@ module HTTPX
|
|
126
134
|
|
127
135
|
# :nocov:
|
128
136
|
def inspect
|
129
|
-
"
|
137
|
+
"#<#{self.class}:#{object_id} " \
|
130
138
|
"HTTP/#{version} " \
|
131
139
|
"@status=#{@status} " \
|
132
140
|
"@headers=#{@headers} " \
|
@@ -275,6 +283,8 @@ module HTTPX
|
|
275
283
|
true
|
276
284
|
end
|
277
285
|
|
286
|
+
def finish!; end
|
287
|
+
|
278
288
|
# raises the wrapped exception.
|
279
289
|
def raise_for_status
|
280
290
|
raise @error
|
data/lib/httpx/selector.rb
CHANGED
@@ -35,14 +35,21 @@ module HTTPX
|
|
35
35
|
end
|
36
36
|
|
37
37
|
begin
|
38
|
-
select(timeout
|
38
|
+
select(timeout) do |c|
|
39
|
+
c.log(level: 2) { "[#{c.state}] selected#{" after #{timeout} secs" unless timeout.nil?}..." }
|
40
|
+
|
41
|
+
c.call
|
42
|
+
end
|
43
|
+
|
39
44
|
@timers.fire
|
40
45
|
rescue TimeoutError => e
|
41
46
|
@timers.fire(e)
|
42
47
|
end
|
43
48
|
end
|
44
49
|
rescue StandardError => e
|
45
|
-
|
50
|
+
each_connection do |c|
|
51
|
+
c.emit(:error, e)
|
52
|
+
end
|
46
53
|
rescue Exception # rubocop:disable Lint/RescueException
|
47
54
|
each_connection do |conn|
|
48
55
|
conn.force_reset
|
@@ -77,9 +84,10 @@ module HTTPX
|
|
77
84
|
return enum_for(__method__) unless block
|
78
85
|
|
79
86
|
@selectables.each do |c|
|
80
|
-
|
87
|
+
case c
|
88
|
+
when Resolver::Resolver
|
81
89
|
c.each_connection(&block)
|
82
|
-
|
90
|
+
when Connection
|
83
91
|
yield c
|
84
92
|
end
|
85
93
|
end
|
@@ -133,6 +141,8 @@ module HTTPX
|
|
133
141
|
@selectables.delete_if do |io|
|
134
142
|
interests = io.interests
|
135
143
|
|
144
|
+
io.log(level: 2) { "[#{io.state}] registering for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}" }
|
145
|
+
|
136
146
|
(r ||= []) << io if READABLE.include?(interests)
|
137
147
|
(w ||= []) << io if WRITABLE.include?(interests)
|
138
148
|
|
@@ -169,6 +179,8 @@ module HTTPX
|
|
169
179
|
|
170
180
|
interests = io.interests
|
171
181
|
|
182
|
+
io.log(level: 2) { "[#{io.state}] registering for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}" }
|
183
|
+
|
172
184
|
result = case interests
|
173
185
|
when :r then io.to_io.wait_readable(interval)
|
174
186
|
when :w then io.to_io.wait_writable(interval)
|
@@ -205,13 +217,5 @@ module HTTPX
|
|
205
217
|
|
206
218
|
connection_interval
|
207
219
|
end
|
208
|
-
|
209
|
-
def emit_error(e)
|
210
|
-
@selectables.each do |c|
|
211
|
-
next if c.is_a?(Resolver::Resolver)
|
212
|
-
|
213
|
-
c.emit(:error, e)
|
214
|
-
end
|
215
|
-
end
|
216
220
|
end
|
217
221
|
end
|
data/lib/httpx/session.rb
CHANGED
@@ -15,11 +15,11 @@ module HTTPX
|
|
15
15
|
# When pass a block, it'll yield itself to it, then closes after the block is evaluated.
|
16
16
|
def initialize(options = EMPTY_HASH, &blk)
|
17
17
|
@options = self.class.default_options.merge(options)
|
18
|
-
@responses = {}
|
19
18
|
@persistent = @options.persistent
|
20
19
|
@pool = @options.pool_class.new(@options.pool_options)
|
21
20
|
@wrapped = false
|
22
21
|
@closing = false
|
22
|
+
INSTANCES[self] = self if @persistent && @options.close_on_fork && INSTANCES
|
23
23
|
wrap(&blk) if blk
|
24
24
|
end
|
25
25
|
|
@@ -174,11 +174,15 @@ module HTTPX
|
|
174
174
|
# returns the HTTPX::Connection through which the +request+ should be sent through.
|
175
175
|
def find_connection(request_uri, selector, options)
|
176
176
|
if (connection = selector.find_connection(request_uri, options))
|
177
|
+
connection.idling if connection.state == :closed
|
178
|
+
connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
|
177
179
|
return connection
|
178
180
|
end
|
179
181
|
|
180
182
|
connection = @pool.checkout_connection(request_uri, options)
|
181
183
|
|
184
|
+
connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" }
|
185
|
+
|
182
186
|
case connection.state
|
183
187
|
when :idle
|
184
188
|
do_init_connection(connection, selector)
|
@@ -207,11 +211,6 @@ module HTTPX
|
|
207
211
|
end
|
208
212
|
end
|
209
213
|
|
210
|
-
# callback executed when a response for a given request has been received.
|
211
|
-
def on_response(request, response)
|
212
|
-
@responses[request] = response
|
213
|
-
end
|
214
|
-
|
215
214
|
# callback executed when an HTTP/2 promise frame has been received.
|
216
215
|
def on_promise(_, stream)
|
217
216
|
log(level: 2) { "#{stream.id}: refusing stream!" }
|
@@ -220,7 +219,9 @@ module HTTPX
|
|
220
219
|
|
221
220
|
# returns the corresponding HTTP::Response to the given +request+ if it has been received.
|
222
221
|
def fetch_response(request, _selector, _options)
|
223
|
-
|
222
|
+
response = request.response
|
223
|
+
|
224
|
+
response if response && response.finished?
|
224
225
|
end
|
225
226
|
|
226
227
|
# sends the +request+ to the corresponding HTTPX::Connection
|
@@ -237,7 +238,9 @@ module HTTPX
|
|
237
238
|
|
238
239
|
raise error unless error.is_a?(Error)
|
239
240
|
|
240
|
-
|
241
|
+
response = ErrorResponse.new(request, error)
|
242
|
+
request.response = response
|
243
|
+
request.emit(:response, response)
|
241
244
|
end
|
242
245
|
|
243
246
|
# returns a set of HTTPX::Request objects built from the given +args+ and +options+.
|
@@ -267,7 +270,6 @@ module HTTPX
|
|
267
270
|
end
|
268
271
|
|
269
272
|
def set_request_callbacks(request)
|
270
|
-
request.on(:response, &method(:on_response).curry(2)[request])
|
271
273
|
request.on(:promise, &method(:on_promise))
|
272
274
|
end
|
273
275
|
|
@@ -362,15 +364,12 @@ module HTTPX
|
|
362
364
|
|
363
365
|
return select_connection(connection, selector) unless found_connection
|
364
366
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
found_connection.once(:open) do
|
369
|
-
next unless found_connection.current_session == self
|
370
|
-
|
371
|
-
coalesce_connections(found_connection, connection, selector, from_pool)
|
372
|
-
end
|
367
|
+
connection.log(level: 2) do
|
368
|
+
"try coalescing from #{from_pool ? "pool##{@pool.object_id}" : "selector##{selector.object_id}"} " \
|
369
|
+
"(conn##{found_connection.object_id}[#{found_connection.origin}])"
|
373
370
|
end
|
371
|
+
|
372
|
+
coalesce_connections(found_connection, connection, selector, from_pool)
|
374
373
|
end
|
375
374
|
|
376
375
|
def on_resolver_close(resolver, selector)
|
@@ -396,14 +395,16 @@ module HTTPX
|
|
396
395
|
# (it is known via +from_pool+), then it adds its to the +selector+.
|
397
396
|
def coalesce_connections(conn1, conn2, selector, from_pool)
|
398
397
|
unless conn1.coalescable?(conn2)
|
398
|
+
conn2.log(level: 2) { "not coalescing with conn##{conn1.object_id}[#{conn1.origin}])" }
|
399
399
|
select_connection(conn2, selector)
|
400
400
|
@pool.checkin_connection(conn1) if from_pool
|
401
401
|
return false
|
402
402
|
end
|
403
403
|
|
404
|
-
conn2.
|
404
|
+
conn2.log(level: 2) { "coalescing with conn##{conn1.object_id}[#{conn1.origin}])" }
|
405
|
+
conn2.coalesce!(conn1)
|
405
406
|
select_connection(conn1, selector) if from_pool
|
406
|
-
|
407
|
+
conn2.disconnect
|
407
408
|
true
|
408
409
|
end
|
409
410
|
|
@@ -448,6 +449,7 @@ module HTTPX
|
|
448
449
|
# session_with_custom = session.plugin(CustomPlugin)
|
449
450
|
#
|
450
451
|
def plugin(pl, options = nil, &block)
|
452
|
+
label = pl
|
451
453
|
# raise Error, "Cannot add a plugin to a frozen config" if frozen?
|
452
454
|
pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol)
|
453
455
|
if !@plugins.include?(pl)
|
@@ -472,9 +474,36 @@ module HTTPX
|
|
472
474
|
@default_options = pl.extra_options(@default_options) if pl.respond_to?(:extra_options)
|
473
475
|
@default_options = @default_options.merge(options) if options
|
474
476
|
|
477
|
+
if pl.respond_to?(:subplugins)
|
478
|
+
pl.subplugins.transform_keys(&Plugins.method(:load_plugin)).each do |main_pl, sub_pl|
|
479
|
+
# in case the main plugin has already been loaded, then apply subplugin functionality
|
480
|
+
# immediately
|
481
|
+
next unless @plugins.include?(main_pl)
|
482
|
+
|
483
|
+
plugin(sub_pl, options, &block)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
475
487
|
pl.configure(self, &block) if pl.respond_to?(:configure)
|
476
488
|
|
489
|
+
if label.is_a?(Symbol)
|
490
|
+
# in case an already-loaded plugin complements functionality of
|
491
|
+
# the plugin currently being loaded, loaded it now
|
492
|
+
@plugins.each do |registered_pl|
|
493
|
+
next if registered_pl == pl
|
494
|
+
|
495
|
+
next unless registered_pl.respond_to?(:subplugins)
|
496
|
+
|
497
|
+
sub_pl = registered_pl.subplugins[label]
|
498
|
+
|
499
|
+
next unless sub_pl
|
500
|
+
|
501
|
+
plugin(sub_pl, options, &block)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
477
505
|
@default_options.freeze
|
506
|
+
set_temporary_name("#{superclass}/#{pl}") if respond_to?(:set_temporary_name) # ruby 3.4 only
|
478
507
|
elsif options
|
479
508
|
# this can happen when two plugins are loaded, an one of them calls the other under the hood,
|
480
509
|
# albeit changing some default.
|
@@ -483,9 +512,40 @@ module HTTPX
|
|
483
512
|
|
484
513
|
@default_options.freeze
|
485
514
|
end
|
515
|
+
|
486
516
|
self
|
487
517
|
end
|
488
518
|
end
|
519
|
+
|
520
|
+
# setup of the support for close_on_fork sessions.
|
521
|
+
# adapted from https://github.com/mperham/connection_pool/blob/main/lib/connection_pool.rb#L48
|
522
|
+
if Process.respond_to?(:fork)
|
523
|
+
INSTANCES = ObjectSpace::WeakMap.new
|
524
|
+
private_constant :INSTANCES
|
525
|
+
|
526
|
+
def self.after_fork
|
527
|
+
INSTANCES.each_value(&:close)
|
528
|
+
nil
|
529
|
+
end
|
530
|
+
|
531
|
+
if ::Process.respond_to?(:_fork)
|
532
|
+
module ForkTracker
|
533
|
+
def _fork
|
534
|
+
pid = super
|
535
|
+
Session.after_fork if pid.zero?
|
536
|
+
pid
|
537
|
+
end
|
538
|
+
end
|
539
|
+
Process.singleton_class.prepend(ForkTracker)
|
540
|
+
end
|
541
|
+
else
|
542
|
+
INSTANCES = nil
|
543
|
+
private_constant :INSTANCES
|
544
|
+
|
545
|
+
def self.after_fork
|
546
|
+
# noop
|
547
|
+
end
|
548
|
+
end
|
489
549
|
end
|
490
550
|
|
491
551
|
# session may be overridden by certain adapters.
|
data/lib/httpx/timers.rb
CHANGED
@@ -7,17 +7,16 @@ module HTTPX
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def after(interval_in_secs, cb = nil, &blk)
|
10
|
-
return unless interval_in_secs
|
11
|
-
|
12
10
|
callback = cb || blk
|
13
11
|
|
12
|
+
raise Error, "timer must have a callback" unless callback
|
13
|
+
|
14
14
|
# I'm assuming here that most requests will have the same
|
15
15
|
# request timeout, as in most cases they share common set of
|
16
16
|
# options. A user setting different request timeouts for 100s of
|
17
17
|
# requests will already have a hard time dealing with that.
|
18
|
-
unless (interval = @intervals.
|
18
|
+
unless (interval = @intervals.bsearch { |t| t.interval == interval_in_secs })
|
19
19
|
interval = Interval.new(interval_in_secs)
|
20
|
-
interval.on_empty { @intervals.delete(interval) }
|
21
20
|
@intervals << interval
|
22
21
|
@intervals.sort!
|
23
22
|
end
|
@@ -30,6 +29,8 @@ module HTTPX
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def wait_interval
|
32
|
+
drop_elapsed!
|
33
|
+
|
33
34
|
return if @intervals.empty?
|
34
35
|
|
35
36
|
@next_interval_at = Utils.now
|
@@ -43,11 +44,25 @@ module HTTPX
|
|
43
44
|
|
44
45
|
elapsed_time = Utils.elapsed_time(@next_interval_at)
|
45
46
|
|
47
|
+
drop_elapsed!(elapsed_time)
|
48
|
+
|
46
49
|
@intervals = @intervals.drop_while { |interval| interval.elapse(elapsed_time) <= 0 }
|
47
50
|
|
48
51
|
@next_interval_at = nil if @intervals.empty?
|
49
52
|
end
|
50
53
|
|
54
|
+
private
|
55
|
+
|
56
|
+
def drop_elapsed!(elapsed_time = 0)
|
57
|
+
# check first, if not elapsed, then return
|
58
|
+
first_interval = @intervals.first
|
59
|
+
|
60
|
+
return unless first_interval && first_interval.elapsed?(elapsed_time)
|
61
|
+
|
62
|
+
# TODO: would be nice to have a drop_while!
|
63
|
+
@intervals = @intervals.drop_while { |interval| interval.elapse(elapsed_time) <= 0 }
|
64
|
+
end
|
65
|
+
|
51
66
|
class Timer
|
52
67
|
def initialize(interval, callback)
|
53
68
|
@interval = interval
|
@@ -67,15 +82,6 @@ module HTTPX
|
|
67
82
|
def initialize(interval)
|
68
83
|
@interval = interval
|
69
84
|
@callbacks = []
|
70
|
-
@on_empty = nil
|
71
|
-
end
|
72
|
-
|
73
|
-
def on_empty(&blk)
|
74
|
-
@on_empty = blk
|
75
|
-
end
|
76
|
-
|
77
|
-
def cancel
|
78
|
-
@on_empty.call
|
79
85
|
end
|
80
86
|
|
81
87
|
def <=>(other)
|
@@ -98,18 +104,20 @@ module HTTPX
|
|
98
104
|
|
99
105
|
def delete(callback)
|
100
106
|
@callbacks.delete(callback)
|
101
|
-
@on_empty.call if @callbacks.empty?
|
102
107
|
end
|
103
108
|
|
104
109
|
def no_callbacks?
|
105
110
|
@callbacks.empty?
|
106
111
|
end
|
107
112
|
|
108
|
-
def elapsed?
|
109
|
-
@interval <= 0
|
113
|
+
def elapsed?(elapsed = 0)
|
114
|
+
(@interval - elapsed) <= 0 || @callbacks.empty?
|
110
115
|
end
|
111
116
|
|
112
117
|
def elapse(elapsed)
|
118
|
+
# same as elapsing
|
119
|
+
return 0 if @callbacks.empty?
|
120
|
+
|
113
121
|
@interval -= elapsed
|
114
122
|
|
115
123
|
if @interval <= 0
|
@@ -12,6 +12,7 @@ module HTTPX
|
|
12
12
|
def initialize(filename, content_type)
|
13
13
|
@original_filename = filename
|
14
14
|
@content_type = content_type
|
15
|
+
@current = nil
|
15
16
|
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
16
17
|
super(@file)
|
17
18
|
end
|
@@ -68,11 +69,12 @@ module HTTPX
|
|
68
69
|
# raise Error, "couldn't parse part headers" unless idx
|
69
70
|
return unless idx
|
70
71
|
|
72
|
+
# @type var head: String
|
71
73
|
head = @buffer.byteslice(0..idx + 4 - 1)
|
72
74
|
|
73
75
|
@buffer = @buffer.byteslice(head.bytesize..-1)
|
74
76
|
|
75
|
-
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
77
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1] || "text/plain"
|
76
78
|
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
77
79
|
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
78
80
|
name.gsub!(/\\(.)/, "\\1")
|
@@ -83,7 +85,7 @@ module HTTPX
|
|
83
85
|
|
84
86
|
filename = HTTPX::Utils.get_filename(head)
|
85
87
|
|
86
|
-
name = filename || +"#{content_type
|
88
|
+
name = filename || +"#{content_type}[]" if name.nil? || name.empty?
|
87
89
|
|
88
90
|
@current = name
|
89
91
|
|
@@ -20,7 +20,7 @@ module HTTPX
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def to_s
|
23
|
-
read
|
23
|
+
read || ""
|
24
24
|
ensure
|
25
25
|
rewind
|
26
26
|
end
|
@@ -37,6 +37,7 @@ module HTTPX
|
|
37
37
|
def rewind
|
38
38
|
form = @form.each_with_object([]) do |(key, val), aux|
|
39
39
|
if val.respond_to?(:path) && val.respond_to?(:reopen) && val.respond_to?(:closed?) && val.closed?
|
40
|
+
# @type var val: File
|
40
41
|
val = val.reopen(val.path, File::RDONLY)
|
41
42
|
end
|
42
43
|
val.rewind if val.respond_to?(:rewind)
|
data/lib/httpx/version.rb
CHANGED