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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_5_0.md +126 -0
  3. data/lib/httpx/adapters/datadog.rb +24 -3
  4. data/lib/httpx/adapters/webmock.rb +1 -0
  5. data/lib/httpx/buffer.rb +16 -5
  6. data/lib/httpx/connection/http1.rb +8 -9
  7. data/lib/httpx/connection/http2.rb +48 -24
  8. data/lib/httpx/connection.rb +36 -19
  9. data/lib/httpx/errors.rb +2 -11
  10. data/lib/httpx/headers.rb +24 -23
  11. data/lib/httpx/io/ssl.rb +2 -1
  12. data/lib/httpx/io/tcp.rb +9 -7
  13. data/lib/httpx/io/unix.rb +1 -1
  14. data/lib/httpx/loggable.rb +13 -1
  15. data/lib/httpx/options.rb +63 -48
  16. data/lib/httpx/parser/http1.rb +1 -1
  17. data/lib/httpx/plugins/aws_sigv4.rb +1 -0
  18. data/lib/httpx/plugins/callbacks.rb +19 -6
  19. data/lib/httpx/plugins/circuit_breaker.rb +4 -3
  20. data/lib/httpx/plugins/cookies/jar.rb +0 -2
  21. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +7 -4
  22. data/lib/httpx/plugins/cookies.rb +4 -4
  23. data/lib/httpx/plugins/follow_redirects.rb +4 -2
  24. data/lib/httpx/plugins/grpc/call.rb +1 -1
  25. data/lib/httpx/plugins/h2c.rb +7 -1
  26. data/lib/httpx/plugins/persistent.rb +22 -1
  27. data/lib/httpx/plugins/proxy/http.rb +3 -1
  28. data/lib/httpx/plugins/query.rb +35 -0
  29. data/lib/httpx/plugins/response_cache/file_store.rb +115 -15
  30. data/lib/httpx/plugins/response_cache/store.rb +7 -67
  31. data/lib/httpx/plugins/response_cache.rb +179 -29
  32. data/lib/httpx/plugins/retries.rb +26 -14
  33. data/lib/httpx/plugins/stream.rb +4 -2
  34. data/lib/httpx/plugins/stream_bidi.rb +315 -0
  35. data/lib/httpx/pool.rb +58 -5
  36. data/lib/httpx/request/body.rb +1 -1
  37. data/lib/httpx/request.rb +6 -2
  38. data/lib/httpx/resolver/https.rb +10 -4
  39. data/lib/httpx/resolver/native.rb +13 -13
  40. data/lib/httpx/resolver/resolver.rb +4 -0
  41. data/lib/httpx/resolver/system.rb +37 -14
  42. data/lib/httpx/resolver.rb +2 -2
  43. data/lib/httpx/response/body.rb +10 -21
  44. data/lib/httpx/response/buffer.rb +36 -12
  45. data/lib/httpx/response.rb +11 -1
  46. data/lib/httpx/selector.rb +16 -12
  47. data/lib/httpx/session.rb +79 -19
  48. data/lib/httpx/timers.rb +24 -16
  49. data/lib/httpx/transcoder/multipart/decoder.rb +4 -2
  50. data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
  51. data/lib/httpx/version.rb +1 -1
  52. data/sig/buffer.rbs +1 -1
  53. data/sig/chainable.rbs +5 -2
  54. data/sig/connection/http2.rbs +11 -2
  55. data/sig/connection.rbs +4 -4
  56. data/sig/errors.rbs +0 -3
  57. data/sig/headers.rbs +15 -10
  58. data/sig/httpx.rbs +5 -1
  59. data/sig/io/tcp.rbs +6 -0
  60. data/sig/loggable.rbs +2 -0
  61. data/sig/options.rbs +7 -1
  62. data/sig/plugins/cookies/cookie.rbs +1 -3
  63. data/sig/plugins/cookies/jar.rbs +4 -4
  64. data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
  65. data/sig/plugins/cookies.rbs +2 -0
  66. data/sig/plugins/h2c.rbs +4 -0
  67. data/sig/plugins/proxy/http.rbs +3 -0
  68. data/sig/plugins/proxy.rbs +4 -0
  69. data/sig/plugins/query.rbs +18 -0
  70. data/sig/plugins/response_cache/file_store.rbs +19 -0
  71. data/sig/plugins/response_cache/store.rbs +13 -0
  72. data/sig/plugins/response_cache.rbs +41 -19
  73. data/sig/plugins/retries.rbs +4 -3
  74. data/sig/plugins/stream.rbs +5 -1
  75. data/sig/plugins/stream_bidi.rbs +68 -0
  76. data/sig/plugins/upgrade/h2.rbs +9 -0
  77. data/sig/plugins/upgrade.rbs +5 -0
  78. data/sig/pool.rbs +5 -0
  79. data/sig/punycode.rbs +5 -0
  80. data/sig/request.rbs +2 -0
  81. data/sig/resolver/https.rbs +3 -2
  82. data/sig/resolver/native.rbs +1 -2
  83. data/sig/resolver/resolver.rbs +11 -3
  84. data/sig/resolver/system.rbs +19 -2
  85. data/sig/resolver.rbs +11 -7
  86. data/sig/response/body.rbs +3 -4
  87. data/sig/response/buffer.rbs +2 -3
  88. data/sig/response.rbs +2 -2
  89. data/sig/selector.rbs +20 -10
  90. data/sig/session.rbs +14 -6
  91. data/sig/timers.rbs +5 -7
  92. data/sig/transcoder/multipart.rbs +4 -3
  93. 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(nil, options)
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
- on_error(error)
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
- @queries.delete(pair)
135
- _, connection = pair
136
- @connections.delete(connection)
149
+ if pair
150
+ @queries.delete(pair)
151
+ family, connection = pair
152
+ @connections.delete(connection)
137
153
 
138
- family, connection = pair
139
- catch(:coalesced) { emit_addresses(connection, family, addrs) }
154
+ catch(:coalesced) { emit_addresses(connection, family, addrs) }
155
+ end
140
156
  when ERROR
141
157
  *pair, error = @pipe_mutex.synchronize { @ips.pop }
142
- @queries.delete(pair)
143
- @connections.delete(connection)
158
+ if pair && error
159
+ @queries.delete(pair)
160
+ @connections.delete(connection)
144
161
 
145
- _, connection = pair
146
- emit_resolve_error(connection, connection.peer.host, error)
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 = @connections.first)
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 = connection.peer.host
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}"
@@ -92,8 +92,8 @@ module HTTPX
92
92
  end
93
93
 
94
94
  ips = entries.flat_map do |address|
95
- if address.key?("alias")
96
- lookup(address["alias"], lookups, ttl)
95
+ if (als = address["alias"])
96
+ lookup(als, lookups, ttl)
97
97
  else
98
98
  IPAddr.new(address["data"])
99
99
  end
@@ -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
- object_id == other.object_id || begin
152
- if other.respond_to?(:read)
153
- _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
154
- else
155
- to_s == other.to_s
156
- end
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
- "#<HTTPX::Response::Body:#{object_id} " \
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
- @buffer = other.instance_variable_get(:@buffer).dup
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 = _with_same_buffer_pos { @buffer.read }
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
@@ -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
- "#<Response:#{object_id} " \
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
@@ -35,14 +35,21 @@ module HTTPX
35
35
  end
36
36
 
37
37
  begin
38
- select(timeout, &:call)
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
- emit_error(e)
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
- if c.is_a?(Resolver::Resolver)
87
+ case c
88
+ when Resolver::Resolver
81
89
  c.each_connection(&block)
82
- else
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
- @responses.delete(request)
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
- request.emit(:response, ErrorResponse.new(request, error))
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
- if found_connection.open?
366
- coalesce_connections(found_connection, connection, selector, from_pool)
367
- else
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.coalesced_connection = conn1
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
- deselect_connection(conn2, selector)
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.find { |t| t.interval == interval_in_secs })
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 || "text/plain"}[]" if name.nil? || name.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.4.4"
4
+ VERSION = "1.5.0"
5
5
  end