httpx 1.2.6 → 1.4.4

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_3_0.md +18 -0
  4. data/doc/release_notes/1_3_1.md +17 -0
  5. data/doc/release_notes/1_3_2.md +6 -0
  6. data/doc/release_notes/1_3_3.md +5 -0
  7. data/doc/release_notes/1_3_4.md +6 -0
  8. data/doc/release_notes/1_4_0.md +43 -0
  9. data/doc/release_notes/1_4_1.md +19 -0
  10. data/doc/release_notes/1_4_2.md +20 -0
  11. data/doc/release_notes/1_4_3.md +11 -0
  12. data/doc/release_notes/1_4_4.md +14 -0
  13. data/lib/httpx/adapters/datadog.rb +56 -80
  14. data/lib/httpx/adapters/faraday.rb +5 -2
  15. data/lib/httpx/adapters/webmock.rb +24 -8
  16. data/lib/httpx/callbacks.rb +2 -7
  17. data/lib/httpx/chainable.rb +3 -1
  18. data/lib/httpx/connection/http1.rb +11 -7
  19. data/lib/httpx/connection/http2.rb +57 -34
  20. data/lib/httpx/connection.rb +270 -71
  21. data/lib/httpx/errors.rb +15 -4
  22. data/lib/httpx/io/ssl.rb +6 -3
  23. data/lib/httpx/io/tcp.rb +1 -1
  24. data/lib/httpx/io/unix.rb +1 -1
  25. data/lib/httpx/loggable.rb +17 -10
  26. data/lib/httpx/options.rb +30 -23
  27. data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
  28. data/lib/httpx/plugins/aws_sigv4.rb +36 -17
  29. data/lib/httpx/plugins/callbacks.rb +13 -2
  30. data/lib/httpx/plugins/circuit_breaker.rb +11 -5
  31. data/lib/httpx/plugins/content_digest.rb +202 -0
  32. data/lib/httpx/plugins/cookies.rb +9 -6
  33. data/lib/httpx/plugins/digest_auth.rb +3 -0
  34. data/lib/httpx/plugins/expect.rb +10 -4
  35. data/lib/httpx/plugins/follow_redirects.rb +68 -33
  36. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  37. data/lib/httpx/plugins/grpc.rb +2 -2
  38. data/lib/httpx/plugins/h2c.rb +23 -20
  39. data/lib/httpx/plugins/internal_telemetry.rb +48 -1
  40. data/lib/httpx/plugins/oauth.rb +1 -1
  41. data/lib/httpx/plugins/persistent.rb +16 -0
  42. data/lib/httpx/plugins/proxy/http.rb +19 -16
  43. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  44. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  45. data/lib/httpx/plugins/proxy.rb +96 -85
  46. data/lib/httpx/plugins/retries.rb +28 -10
  47. data/lib/httpx/plugins/ssrf_filter.rb +4 -1
  48. data/lib/httpx/plugins/stream.rb +42 -18
  49. data/lib/httpx/plugins/upgrade.rb +5 -10
  50. data/lib/httpx/plugins/webdav.rb +6 -0
  51. data/lib/httpx/plugins/xml.rb +76 -0
  52. data/lib/httpx/pool.rb +73 -244
  53. data/lib/httpx/request/body.rb +50 -55
  54. data/lib/httpx/request.rb +77 -14
  55. data/lib/httpx/resolver/https.rb +17 -20
  56. data/lib/httpx/resolver/multi.rb +34 -16
  57. data/lib/httpx/resolver/native.rb +140 -61
  58. data/lib/httpx/resolver/resolver.rb +64 -19
  59. data/lib/httpx/resolver/system.rb +32 -16
  60. data/lib/httpx/resolver.rb +21 -14
  61. data/lib/httpx/response/body.rb +12 -1
  62. data/lib/httpx/response.rb +16 -9
  63. data/lib/httpx/selector.rb +170 -91
  64. data/lib/httpx/session.rb +282 -139
  65. data/lib/httpx/timers.rb +17 -2
  66. data/lib/httpx/transcoder/body.rb +15 -29
  67. data/lib/httpx/transcoder/form.rb +2 -0
  68. data/lib/httpx/transcoder/gzip.rb +0 -3
  69. data/lib/httpx/transcoder/json.rb +16 -2
  70. data/lib/httpx/transcoder/multipart/encoder.rb +11 -2
  71. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  72. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  73. data/lib/httpx/transcoder.rb +0 -1
  74. data/lib/httpx/version.rb +1 -1
  75. data/lib/httpx.rb +20 -21
  76. data/sig/callbacks.rbs +2 -3
  77. data/sig/chainable.rbs +6 -2
  78. data/sig/connection/http1.rbs +2 -2
  79. data/sig/connection/http2.rbs +22 -18
  80. data/sig/connection.rbs +40 -9
  81. data/sig/errors.rbs +9 -3
  82. data/sig/httpx.rbs +3 -3
  83. data/sig/io/tcp.rbs +1 -1
  84. data/sig/io/unix.rbs +1 -1
  85. data/sig/loggable.rbs +4 -2
  86. data/sig/options.rbs +8 -13
  87. data/sig/plugins/aws_sigv4.rbs +8 -2
  88. data/sig/plugins/content_digest.rbs +51 -0
  89. data/sig/plugins/cookies/cookie.rbs +9 -0
  90. data/sig/plugins/follow_redirects.rbs +1 -1
  91. data/sig/plugins/grpc/call.rbs +4 -0
  92. data/sig/plugins/persistent.rbs +4 -1
  93. data/sig/plugins/proxy/http.rbs +3 -0
  94. data/sig/plugins/proxy/socks5.rbs +11 -3
  95. data/sig/plugins/proxy.rbs +18 -9
  96. data/sig/plugins/push_promise.rbs +6 -3
  97. data/sig/plugins/rate_limiter.rbs +2 -0
  98. data/sig/plugins/retries.rbs +1 -1
  99. data/sig/plugins/ssrf_filter.rbs +26 -0
  100. data/sig/plugins/stream.rbs +3 -0
  101. data/sig/plugins/webdav.rbs +23 -0
  102. data/sig/plugins/xml.rbs +37 -0
  103. data/sig/pool.rbs +27 -33
  104. data/sig/request/body.rbs +4 -10
  105. data/sig/request.rbs +14 -1
  106. data/sig/resolver/multi.rbs +26 -1
  107. data/sig/resolver/native.rbs +6 -3
  108. data/sig/resolver/resolver.rbs +22 -3
  109. data/sig/resolver.rbs +5 -1
  110. data/sig/response/body.rbs +2 -2
  111. data/sig/response/buffer.rbs +2 -2
  112. data/sig/response.rbs +9 -4
  113. data/sig/selector.rbs +31 -4
  114. data/sig/session.rbs +54 -20
  115. data/sig/timers.rbs +15 -4
  116. data/sig/transcoder/body.rbs +2 -4
  117. data/sig/transcoder/chunker.rbs +1 -1
  118. data/sig/transcoder/deflate.rbs +1 -0
  119. data/sig/transcoder/form.rbs +8 -0
  120. data/sig/transcoder/gzip.rbs +4 -1
  121. data/sig/transcoder/json.rbs +1 -1
  122. data/sig/transcoder/multipart.rbs +6 -4
  123. data/sig/transcoder/utils/body_reader.rbs +3 -3
  124. data/sig/transcoder/utils/deflater.rbs +2 -3
  125. metadata +32 -14
  126. data/lib/httpx/session2.rb +0 -23
  127. data/lib/httpx/transcoder/utils/inflater.rb +0 -19
  128. data/lib/httpx/transcoder/xml.rb +0 -52
  129. data/sig/transcoder/utils/inflater.rbs +0 -12
  130. data/sig/transcoder/xml.rbs +0 -22
@@ -53,8 +53,8 @@ module HTTPX
53
53
 
54
54
  def cached_lookup(hostname)
55
55
  now = Utils.now
56
- @lookup_mutex.synchronize do
57
- lookup(hostname, now)
56
+ lookup_synchronize do |lookups|
57
+ lookup(hostname, lookups, now)
58
58
  end
59
59
  end
60
60
 
@@ -63,37 +63,37 @@ module HTTPX
63
63
  entries.each do |entry|
64
64
  entry["TTL"] += now
65
65
  end
66
- @lookup_mutex.synchronize do
66
+ lookup_synchronize do |lookups|
67
67
  case family
68
68
  when Socket::AF_INET6
69
- @lookups[hostname].concat(entries)
69
+ lookups[hostname].concat(entries)
70
70
  when Socket::AF_INET
71
- @lookups[hostname].unshift(*entries)
71
+ lookups[hostname].unshift(*entries)
72
72
  end
73
73
  entries.each do |entry|
74
74
  next unless entry["name"] != hostname
75
75
 
76
76
  case family
77
77
  when Socket::AF_INET6
78
- @lookups[entry["name"]] << entry
78
+ lookups[entry["name"]] << entry
79
79
  when Socket::AF_INET
80
- @lookups[entry["name"]].unshift(entry)
80
+ lookups[entry["name"]].unshift(entry)
81
81
  end
82
82
  end
83
83
  end
84
84
  end
85
85
 
86
86
  # do not use directly!
87
- def lookup(hostname, ttl)
88
- return unless @lookups.key?(hostname)
87
+ def lookup(hostname, lookups, ttl)
88
+ return unless lookups.key?(hostname)
89
89
 
90
- entries = @lookups[hostname] = @lookups[hostname].select do |address|
90
+ entries = lookups[hostname] = lookups[hostname].select do |address|
91
91
  address["TTL"] > ttl
92
92
  end
93
93
 
94
94
  ips = entries.flat_map do |address|
95
95
  if address.key?("alias")
96
- lookup(address["alias"], ttl)
96
+ lookup(address["alias"], lookups, ttl)
97
97
  else
98
98
  IPAddr.new(address["data"])
99
99
  end
@@ -103,12 +103,11 @@ module HTTPX
103
103
  end
104
104
 
105
105
  def generate_id
106
- @identifier_mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
106
+ id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
107
107
  end
108
108
 
109
109
  def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
110
- Resolv::DNS::Message.new.tap do |query|
111
- query.id = message_id
110
+ Resolv::DNS::Message.new(message_id).tap do |query|
112
111
  query.rd = 1
113
112
  query.add_question(hostname, type)
114
113
  end.encode
@@ -150,5 +149,13 @@ module HTTPX
150
149
 
151
150
  [:ok, addresses]
152
151
  end
152
+
153
+ def lookup_synchronize
154
+ @lookup_mutex.synchronize { yield(@lookups) }
155
+ end
156
+
157
+ def id_synchronize(&block)
158
+ @identifier_mutex.synchronize(&block)
159
+ end
153
160
  end
154
161
  end
@@ -17,12 +17,23 @@ module HTTPX
17
17
  @headers = response.headers
18
18
  @options = options
19
19
  @window_size = options.window_size
20
- @encoding = response.content_type.charset || Encoding::BINARY
21
20
  @encodings = []
22
21
  @length = 0
23
22
  @buffer = nil
24
23
  @reader = nil
25
24
  @state = :idle
25
+
26
+ # initialize response encoding
27
+ @encoding = if (enc = response.content_type.charset)
28
+ begin
29
+ Encoding.find(enc)
30
+ rescue ArgumentError
31
+ Encoding::BINARY
32
+ end
33
+ else
34
+ Encoding::BINARY
35
+ end
36
+
26
37
  initialize_inflaters
27
38
  end
28
39
 
@@ -52,9 +52,6 @@ module HTTPX
52
52
  # copies the response body to a different location.
53
53
  def_delegator :@body, :copy_to
54
54
 
55
- # closes the body.
56
- def_delegator :@body, :close
57
-
58
55
  # the corresponding request uri.
59
56
  def_delegator :@request, :uri
60
57
 
@@ -74,6 +71,12 @@ module HTTPX
74
71
  @content_type = nil
75
72
  end
76
73
 
74
+ # closes the respective +@request+ and +@body+.
75
+ def close
76
+ @request.close
77
+ @body.close
78
+ end
79
+
77
80
  # merges headers defined in +h+ into the response headers.
78
81
  def merge_headers(h)
79
82
  @headers = @headers.merge(h)
@@ -166,10 +169,12 @@ module HTTPX
166
169
  decode(Transcoder::Form)
167
170
  end
168
171
 
169
- # decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
170
- # "application/xml" (requires the "nokogiri" gem).
171
172
  def xml
172
- decode(Transcoder::Xml)
173
+ # TODO: remove at next major version.
174
+ warn "DEPRECATION WARNING: calling `.#{__method__}` on plain HTTPX responses is deprecated. " \
175
+ "Use HTTPX.plugin(:xml) sessions and call `.#{__method__}` in its responses instead."
176
+ require "httpx/plugins/xml"
177
+ decode(Plugins::XML::Transcoder)
173
178
  end
174
179
 
175
180
  private
@@ -247,11 +252,11 @@ module HTTPX
247
252
  # the IP address of the peer server.
248
253
  def_delegator :@request, :peer_address
249
254
 
250
- def initialize(request, error, options)
255
+ def initialize(request, error)
251
256
  @request = request
252
257
  @response = request.response if request.response.is_a?(Response)
253
258
  @error = error
254
- @options = Options.new(options)
259
+ @options = request.options
255
260
  log_exception(@error)
256
261
  end
257
262
 
@@ -262,7 +267,7 @@ module HTTPX
262
267
 
263
268
  # closes the error resources.
264
269
  def close
265
- @response.close if @response && @response.respond_to?(:close)
270
+ @response.close if @response
266
271
  end
267
272
 
268
273
  # always true for error responses.
@@ -277,6 +282,8 @@ module HTTPX
277
282
 
278
283
  # buffers lost chunks to error response
279
284
  def <<(data)
285
+ return unless @response
286
+
280
287
  @response << data
281
288
  end
282
289
  end
@@ -2,71 +2,142 @@
2
2
 
3
3
  require "io/wait"
4
4
 
5
- class HTTPX::Selector
6
- READABLE = %i[rw r].freeze
7
- WRITABLE = %i[rw w].freeze
5
+ module HTTPX
6
+ class Selector
7
+ extend Forwardable
8
8
 
9
- private_constant :READABLE
10
- private_constant :WRITABLE
9
+ READABLE = %i[rw r].freeze
10
+ WRITABLE = %i[rw w].freeze
11
11
 
12
- def initialize
13
- @selectables = []
14
- end
12
+ private_constant :READABLE
13
+ private_constant :WRITABLE
15
14
 
16
- # deregisters +io+ from selectables.
17
- def deregister(io)
18
- @selectables.delete(io)
19
- end
15
+ def_delegator :@timers, :after
20
16
 
21
- # register +io+.
22
- def register(io)
23
- return if @selectables.include?(io)
17
+ def_delegator :@selectables, :empty?
24
18
 
25
- @selectables << io
26
- end
19
+ def initialize
20
+ @timers = Timers.new
21
+ @selectables = []
22
+ @is_timer_interval = false
23
+ end
27
24
 
28
- private
25
+ def each(&blk)
26
+ @selectables.each(&blk)
27
+ end
29
28
 
30
- def select_many(interval, &block)
31
- selectables, r, w = nil
29
+ def next_tick
30
+ catch(:jump_tick) do
31
+ timeout = next_timeout
32
+ if timeout && timeout.negative?
33
+ @timers.fire
34
+ throw(:jump_tick)
35
+ end
32
36
 
33
- # first, we group IOs based on interest type. On call to #interests however,
34
- # things might already happen, and new IOs might be registered, so we might
35
- # have to start all over again. We do this until we group all selectables
36
- begin
37
- loop do
38
37
  begin
39
- r = nil
40
- w = nil
38
+ select(timeout, &:call)
39
+ @timers.fire
40
+ rescue TimeoutError => e
41
+ @timers.fire(e)
42
+ end
43
+ end
44
+ rescue StandardError => e
45
+ emit_error(e)
46
+ rescue Exception # rubocop:disable Lint/RescueException
47
+ each_connection do |conn|
48
+ conn.force_reset
49
+ conn.disconnect
50
+ end
41
51
 
42
- selectables = @selectables
43
- @selectables = []
52
+ raise
53
+ end
44
54
 
45
- selectables.delete_if do |io|
46
- interests = io.interests
55
+ def terminate
56
+ # array may change during iteration
57
+ selectables = @selectables.reject(&:inflight?)
47
58
 
48
- (r ||= []) << io if READABLE.include?(interests)
49
- (w ||= []) << io if WRITABLE.include?(interests)
59
+ selectables.each(&:terminate)
50
60
 
51
- io.state == :closed
52
- end
61
+ until selectables.empty?
62
+ next_tick
53
63
 
54
- if @selectables.empty?
55
- @selectables = selectables
64
+ selectables &= @selectables
65
+ end
66
+ end
56
67
 
57
- # do not run event loop if there's nothing to wait on.
58
- # this might happen if connect failed and connection was unregistered.
59
- return if (!r || r.empty?) && (!w || w.empty?) && !selectables.empty?
68
+ def find_resolver(options)
69
+ res = @selectables.find do |c|
70
+ c.is_a?(Resolver::Resolver) && options == c.options
71
+ end
72
+
73
+ res.multi if res
74
+ end
60
75
 
61
- break
62
- else
63
- @selectables.concat(selectables)
64
- end
65
- rescue StandardError
66
- @selectables = selectables if selectables
67
- raise
76
+ def each_connection(&block)
77
+ return enum_for(__method__) unless block
78
+
79
+ @selectables.each do |c|
80
+ if c.is_a?(Resolver::Resolver)
81
+ c.each_connection(&block)
82
+ else
83
+ yield c
68
84
  end
69
85
  end
86
+ end
87
+
88
+ def find_connection(request_uri, options)
89
+ each_connection.find do |connection|
90
+ connection.match?(request_uri, options)
91
+ end
92
+ end
93
+
94
+ def find_mergeable_connection(connection)
95
+ each_connection.find do |ch|
96
+ ch != connection && ch.mergeable?(connection)
97
+ end
98
+ end
99
+
100
+ # deregisters +io+ from selectables.
101
+ def deregister(io)
102
+ @selectables.delete(io)
103
+ end
104
+
105
+ # register +io+.
106
+ def register(io)
107
+ return if @selectables.include?(io)
108
+
109
+ @selectables << io
110
+ end
111
+
112
+ private
113
+
114
+ def select(interval, &block)
115
+ # do not cause an infinite loop here.
116
+ #
117
+ # this may happen if timeout calculation actually triggered an error which causes
118
+ # the connections to be reaped (such as the total timeout error) before #select
119
+ # gets called.
120
+ return if interval.nil? && @selectables.empty?
121
+
122
+ return select_one(interval, &block) if @selectables.size == 1
123
+
124
+ select_many(interval, &block)
125
+ end
126
+
127
+ def select_many(interval, &block)
128
+ r, w = nil
129
+
130
+ # first, we group IOs based on interest type. On call to #interests however,
131
+ # things might already happen, and new IOs might be registered, so we might
132
+ # have to start all over again. We do this until we group all selectables
133
+ @selectables.delete_if do |io|
134
+ interests = io.interests
135
+
136
+ (r ||= []) << io if READABLE.include?(interests)
137
+ (w ||= []) << io if WRITABLE.include?(interests)
138
+
139
+ io.state == :closed
140
+ end
70
141
 
71
142
  # TODO: what to do if there are no selectables?
72
143
 
@@ -76,63 +147,71 @@ class HTTPX::Selector
76
147
  [*r, *w].each { |io| io.handle_socket_timeout(interval) }
77
148
  return
78
149
  end
79
- rescue IOError, SystemCallError
80
- @selectables.reject!(&:closed?)
81
- retry
82
- end
83
150
 
84
- if writers
85
- readers.each do |io|
86
- yield io
151
+ if writers
152
+ readers.each do |io|
153
+ yield io
87
154
 
88
- # so that we don't yield 2 times
89
- writers.delete(io)
90
- end if readers
155
+ # so that we don't yield 2 times
156
+ writers.delete(io)
157
+ end if readers
91
158
 
92
- writers.each(&block)
93
- else
94
- readers.each(&block) if readers
159
+ writers.each(&block)
160
+ else
161
+ readers.each(&block) if readers
162
+ end
95
163
  end
96
- end
97
164
 
98
- def select_one(interval)
99
- io = @selectables.first
165
+ def select_one(interval)
166
+ io = @selectables.first
100
167
 
101
- return unless io
168
+ return unless io
102
169
 
103
- interests = io.interests
170
+ interests = io.interests
104
171
 
105
- result = case interests
106
- when :r then io.to_io.wait_readable(interval)
107
- when :w then io.to_io.wait_writable(interval)
108
- when :rw then io.to_io.wait(interval, :read_write)
109
- when nil then return
110
- end
172
+ result = case interests
173
+ when :r then io.to_io.wait_readable(interval)
174
+ when :w then io.to_io.wait_writable(interval)
175
+ when :rw then io.to_io.wait(interval, :read_write)
176
+ when nil then return
177
+ end
111
178
 
112
- unless result || interval.nil?
113
- io.handle_socket_timeout(interval)
114
- return
179
+ unless result || interval.nil?
180
+ io.handle_socket_timeout(interval) unless @is_timer_interval
181
+ return
182
+ end
183
+ # raise TimeoutError.new(interval, "timed out while waiting on select")
184
+
185
+ yield io
186
+ # rescue IOError, SystemCallError
187
+ # @selectables.reject!(&:closed?)
188
+ # raise unless @selectables.empty?
115
189
  end
116
- # raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
117
190
 
118
- yield io
119
- rescue IOError, SystemCallError
120
- @selectables.reject!(&:closed?)
121
- raise unless @selectables.empty?
122
- end
191
+ def next_timeout
192
+ @is_timer_interval = false
123
193
 
124
- def select(interval, &block)
125
- # do not cause an infinite loop here.
126
- #
127
- # this may happen if timeout calculation actually triggered an error which causes
128
- # the connections to be reaped (such as the total timeout error) before #select
129
- # gets called.
130
- return if interval.nil? && @selectables.empty?
194
+ timer_interval = @timers.wait_interval
131
195
 
132
- return select_one(interval, &block) if @selectables.size == 1
196
+ connection_interval = @selectables.filter_map(&:timeout).min
133
197
 
134
- select_many(interval, &block)
135
- end
198
+ return connection_interval unless timer_interval
199
+
200
+ if connection_interval.nil? || timer_interval <= connection_interval
201
+ @is_timer_interval = true
202
+
203
+ return timer_interval
204
+ end
205
+
206
+ connection_interval
207
+ end
208
+
209
+ def emit_error(e)
210
+ @selectables.each do |c|
211
+ next if c.is_a?(Resolver::Resolver)
136
212
 
137
- public :select
213
+ c.emit(:error, e)
214
+ end
215
+ end
216
+ end
138
217
  end