httpx 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_1.md +1 -1
  3. data/doc/release_notes/1_8_2.md +4 -4
  4. data/doc/release_notes/1_8_3.md +3 -5
  5. data/doc/release_notes/1_8_4.md +1 -1
  6. data/doc/release_notes/1_9_0.md +7 -7
  7. data/doc/release_notes/1_9_1.md +32 -0
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +14 -1
  10. data/lib/httpx/connection/http1.rb +24 -17
  11. data/lib/httpx/connection/http2.rb +108 -29
  12. data/lib/httpx/connection.rb +158 -162
  13. data/lib/httpx/io/tcp.rb +24 -7
  14. data/lib/httpx/plugins/aws_sigv4.rb +0 -1
  15. data/lib/httpx/plugins/cache.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/content_digest.rb +3 -5
  18. data/lib/httpx/plugins/expect.rb +8 -1
  19. data/lib/httpx/plugins/fiber_concurrency.rb +56 -19
  20. data/lib/httpx/plugins/h2c.rb +12 -11
  21. data/lib/httpx/plugins/hedging.rb +2 -2
  22. data/lib/httpx/plugins/ntlm_auth.rb +4 -3
  23. data/lib/httpx/plugins/ntlm_v2_auth.rb +4 -3
  24. data/lib/httpx/plugins/oauth.rb +1 -1
  25. data/lib/httpx/plugins/proxy/http.rb +23 -16
  26. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  27. data/lib/httpx/plugins/proxy.rb +8 -15
  28. data/lib/httpx/plugins/query.rb +1 -0
  29. data/lib/httpx/plugins/stream.rb +12 -10
  30. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  31. data/lib/httpx/plugins/tracing.rb +62 -21
  32. data/lib/httpx/plugins/upgrade/h2.rb +2 -2
  33. data/lib/httpx/request.rb +1 -1
  34. data/lib/httpx/resolver/https.rb +3 -2
  35. data/lib/httpx/resolver/native.rb +19 -9
  36. data/lib/httpx/resolver/resolver.rb +2 -2
  37. data/lib/httpx/resolver/system.rb +4 -0
  38. data/lib/httpx/resolver.rb +5 -5
  39. data/lib/httpx/response.rb +5 -1
  40. data/lib/httpx/selector.rb +108 -7
  41. data/lib/httpx/transcoder/json.rb +1 -1
  42. data/lib/httpx/version.rb +1 -1
  43. data/lib/httpx.rb +1 -0
  44. data/sig/chainable.rbs +2 -1
  45. data/sig/connection/http1.rbs +4 -2
  46. data/sig/connection/http2.rbs +8 -2
  47. data/sig/connection.rbs +11 -6
  48. data/sig/io/tcp.rbs +5 -0
  49. data/sig/plugins/expect.rbs +6 -0
  50. data/sig/plugins/fiber_concurrency.rbs +10 -1
  51. data/sig/plugins/proxy/http.rbs +3 -1
  52. data/sig/plugins/stream_bidi.rbs +0 -2
  53. data/sig/plugins/tracing.rbs +10 -2
  54. data/sig/resolver/https.rbs +2 -0
  55. data/sig/resolver/native.rbs +3 -0
  56. data/sig/resolver/resolver.rbs +1 -1
  57. data/sig/resolver/system.rbs +2 -0
  58. data/sig/resolver.rbs +1 -1
  59. data/sig/selector.rbs +15 -1
  60. data/sig/session.rbs +1 -1
  61. metadata +3 -1
@@ -81,15 +81,13 @@ module HTTPX
81
81
  end
82
82
 
83
83
  def write(chunk)
84
- @content_digest_buffer.write(chunk) if @content_digest_buffer
84
+ @content_digest_buffer&.write(chunk)
85
85
  super
86
86
  end
87
87
 
88
88
  def close
89
- if @content_digest_buffer
90
- @content_digest_buffer.close
91
- @content_digest_buffer = nil
92
- end
89
+ @content_digest_buffer&.close
90
+ @content_digest_buffer = nil
93
91
  super
94
92
  end
95
93
  end
@@ -104,9 +104,15 @@ module HTTPX
104
104
  end
105
105
 
106
106
  module ConnectionMethods
107
- def send_request_to_parser(request)
107
+ private
108
+
109
+ def set_request_timeouts(request)
108
110
  super
109
111
 
112
+ set_request_expect_timeout(request)
113
+ end
114
+
115
+ def set_request_expect_timeout(request)
110
116
  return unless request.headers["expect"] == "100-continue"
111
117
 
112
118
  expect_timeout = request.options.expect_timeout
@@ -118,6 +124,7 @@ module HTTPX
118
124
  if request.state == :expect && !request.expects?
119
125
  Expect.no_expect_store.add(request.origin)
120
126
  request.headers.delete("expect")
127
+ request.transition(:body)
121
128
  consume
122
129
  end
123
130
  end
@@ -76,10 +76,10 @@ module HTTPX
76
76
  super
77
77
  end
78
78
 
79
- def interests
80
- return if connecting? && @pending.none?(&:current_context?)
81
-
82
- super
79
+ def closed_for_select?
80
+ # @fiber-switch-guard
81
+ # a closed connection still holding requests will reconnect once the fiber closing its socket resumes.
82
+ super && @pending.empty?
83
83
  end
84
84
 
85
85
  def on_io_error(e)
@@ -88,17 +88,12 @@ module HTTPX
88
88
  # @fiber-switch-guard
89
89
  # sockets closed during fiber scheduler switches are raised in separate fibers than the fiber the
90
90
  # socket may be used in. this check verifies that this is actually about this socket.
91
- return unless to_io.closed?
92
-
93
- if @state == :closing
94
- # @fiber-switch-guard
95
- # if the connection is reused across fibers, the socket may have been closed in the other fiber
96
- # and switched here during the process, so continue what it was doing and transition to closed
97
- # via #call.
91
+ unless to_io.closed? && !backlog?
98
92
  call
99
- elsif !backlog?
100
- super
93
+ return
101
94
  end
95
+
96
+ super
102
97
  end
103
98
 
104
99
  def on_connect_error(e)
@@ -107,24 +102,50 @@ module HTTPX
107
102
  # @fiber-switch-guard
108
103
  # sockets closed during fiber scheduler switches are raised in separate fibers than the fiber the
109
104
  # socket may be used in. this check verifies that this is actually about this socket.
110
- return unless to_io.closed? && !backlog?
105
+ unless to_io.closed? && !backlog?
106
+ call
107
+ return
108
+ end
111
109
 
112
110
  super
113
111
  end
114
112
 
115
113
  def pending_count
116
- count = @pending.count(&:current_context?)
117
- count += @parser.pending.count(&:current_context?) if @parser.respond_to?(:pending)
118
- count
114
+ @pending.count(&:current_context?)
115
+ end
116
+
117
+ def interests
118
+ idling if (@state == :closed) && !no_more_requests?
119
+
120
+ super
119
121
  end
120
122
 
121
123
  private
122
124
 
125
+ def handle_transition(nextstate)
126
+ case nextstate
127
+ when :inactive
128
+ # @fiber-switch-guard
129
+ # connections may have requests (pending or inflight) from other fibers.
130
+ return if @state == :open && backlog?
131
+ end
132
+ super
133
+ end
134
+
135
+ def no_requests_to_handle?
136
+ pending_count.zero? && @parser&.no_requests_to_handle?
137
+ end
138
+
139
+ # @fiber-switch-guard
140
+ # pending requests from other fibers are still being waited for.
141
+ def clear_pending_requests
142
+ @pending.reject!(&:current_context?)
143
+ end
144
+
123
145
  # checks whether the connection has any pending request (which the connection itself may
124
146
  # have stored, or it may be somewhere in the parser).
125
147
  def backlog?
126
- @pending.any? ||
127
- (@parser && (@parser.pending.any? || @parser.requests.any?))
148
+ @pending.any? || @parser&.backlog? || @parser&.waiting_for_ping?
128
149
  end
129
150
  end
130
151
 
@@ -138,6 +159,14 @@ module HTTPX
138
159
 
139
160
  super
140
161
  end
162
+
163
+ def backlog?
164
+ !no_more_requests?
165
+ end
166
+
167
+ def no_requests_to_handle?
168
+ @requests.none?(&:current_context?)
169
+ end
141
170
  end
142
171
 
143
172
  module HTTP2Methods
@@ -162,6 +191,14 @@ module HTTPX
162
191
  super
163
192
  end
164
193
 
194
+ def backlog?
195
+ !no_more_requests?
196
+ end
197
+
198
+ def no_requests_to_handle?
199
+ @streams.each_key.none?(&:current_context?)
200
+ end
201
+
165
202
  private
166
203
 
167
204
  def on_close(_, error, _)
@@ -68,29 +68,30 @@ module HTTPX
68
68
  end
69
69
 
70
70
  def upgrade_to_h2c(request, response)
71
- enqueue_pending_requests_from_parser(@parser)
71
+ @parser.reset_requests
72
72
 
73
- @parser = request.options.h2c_class.new(@write_buffer, @options)
73
+ @parser = request.options.h2c_class.new(@write_buffer, @pending, @options)
74
74
  set_parser_callbacks(@parser)
75
- @inflight += 1 # request is being completed below
76
75
  @parser.upgrade(request, response)
77
76
  @upgrade_protocol = "h2c"
78
77
  end
79
78
 
80
79
  private
81
80
 
82
- def send_request_to_parser(request)
83
- super
81
+ def send_request_to_parser(request, *)
82
+ super.tap do |is_inflight|
83
+ next unless is_inflight
84
84
 
85
- return unless request.headers["upgrade"] == "h2c" && parser.is_a?(Connection::HTTP1)
85
+ next unless request.headers["upgrade"] == "h2c" && parser.is_a?(Connection::HTTP1)
86
86
 
87
- max_concurrent_requests = parser.max_concurrent_requests
87
+ max_concurrent_requests = parser.max_concurrent_requests
88
88
 
89
- return if max_concurrent_requests == 1
89
+ next if max_concurrent_requests == 1
90
90
 
91
- parser.max_concurrent_requests = 1
92
- request.once(:response) do
93
- parser.max_concurrent_requests = max_concurrent_requests
91
+ parser.max_concurrent_requests = 1
92
+ request.once(:response) do
93
+ parser.max_concurrent_requests = max_concurrent_requests
94
+ end
94
95
  end
95
96
  end
96
97
  end
@@ -270,10 +270,10 @@ module HTTPX
270
270
 
271
271
  private
272
272
 
273
- def send_request_to_parser(request)
273
+ def send_request_to_parser(request, *)
274
274
  # if original request was in the queue, but has since been finished
275
275
  # in the hedge connection.
276
- return if request.response&.finished?
276
+ return false if request.response&.finished?
277
277
 
278
278
  super
279
279
  end
@@ -41,9 +41,10 @@ module HTTPX
41
41
 
42
42
  if ntlm
43
43
  request.authorize(ntlm.negotiate)
44
- probe_response = wrap { super(request).first }
44
+ responses = wrap { super(request) }
45
+ probe_response = responses.first
45
46
 
46
- return probe_response unless probe_response.is_a?(Response)
47
+ return responses unless probe_response.is_a?(Response)
47
48
 
48
49
  if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
49
50
  request.transition(:idle)
@@ -51,7 +52,7 @@ module HTTPX
51
52
  request.authorize(ntlm.authenticate(request, probe_response.headers["www-authenticate"]).encode("utf-8"))
52
53
  super(request)
53
54
  else
54
- probe_response
55
+ responses
55
56
  end
56
57
  else
57
58
  super(request)
@@ -67,9 +67,10 @@ module HTTPX
67
67
 
68
68
  if ntlm
69
69
  request.authorize(ntlm.negotiate)
70
- probe_response = wrap { super(request).first }
70
+ responses = wrap { super(request) }
71
+ probe_response = responses.first
71
72
 
72
- return probe_response unless probe_response.is_a?(Response)
73
+ return responses unless probe_response.is_a?(Response)
73
74
 
74
75
  if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
75
76
  request.transition(:idle)
@@ -78,7 +79,7 @@ module HTTPX
78
79
  probe_response.headers["www-authenticate"]).encode("utf-8"))
79
80
  super(request)
80
81
  else
81
- probe_response
82
+ responses
82
83
  end
83
84
  else
84
85
  super(request)
@@ -290,7 +290,7 @@ module HTTPX
290
290
 
291
291
  def reset_auth_header_value!
292
292
  super.tap do
293
- @oauth_session.reset if @oauth_session
293
+ @oauth_session&.reset!
294
294
  end
295
295
  end
296
296
 
@@ -16,9 +16,22 @@ module HTTPX
16
16
  # :enable_proxy_tunnel :: when <tt>true</tt>, it forces the establishment of the CONNECT proxy
17
17
  # tunnel, in cases where it wouldn't otherwise normally establish it
18
18
  # (i.e. if the request is an http:// request).
19
+ # :connect_request_class :: class of the request object used for the CONNECT tunnel request.
19
20
  module OptionsMethods
21
+ def extend_with_plugin_classes(pl)
22
+ super
23
+
24
+ @connect_request_class = @request_class.dup
25
+ Options::SET_TEMPORARY_NAME[@connect_request_class, pl]
26
+ @connect_request_class.__send__(:include, ConnectRequestMethods)
27
+ end
28
+
20
29
  private
21
30
 
31
+ def option_connect_request_class(val)
32
+ val
33
+ end
34
+
22
35
  def option_proxy_headers(value)
23
36
  value = value.dup if value.frozen?
24
37
 
@@ -80,8 +93,6 @@ module HTTPX
80
93
  if @state == :connecting
81
94
  # proxy connect related requests should not be reenqueed
82
95
  @parser.reset
83
- @inflight -= @parser.pending.size
84
- @parser.pending.clear
85
96
  end
86
97
 
87
98
  super
@@ -114,12 +125,12 @@ module HTTPX
114
125
  @tunnel_parser || begin
115
126
  if @parser
116
127
  # in case this happens, requests from @parser need to be reenqueued for delivery.
117
- enqueue_pending_requests_from_parser(@parser)
128
+ @parser.reset_requests
118
129
  @parser = nil
119
130
  end
120
131
 
121
132
  @tunnel_parser = @parser = parser =
122
- parser_type(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
133
+ parser_type(@io.protocol).new(@write_buffer, @pending, @options.merge(max_concurrent_requests: 1))
123
134
  parser.extend(ProxyParser)
124
135
  parser.on(:response, &method(:__http_on_connect))
125
136
  parser.on(:close) do
@@ -127,11 +138,12 @@ module HTTPX
127
138
 
128
139
  reset
129
140
  end
141
+ parser.on(:error, &method(:on_parser_error))
130
142
  parser.on(:reset) do
131
- if parser.pending.empty? && parser.empty?
143
+ if parser.empty?
132
144
  reset
133
145
  else
134
- enqueue_pending_requests_from_parser(parser)
146
+ parser.reset_requests
135
147
 
136
148
  initial_state = @state
137
149
 
@@ -152,7 +164,7 @@ module HTTPX
152
164
  if initial_state == :connecting
153
165
  parser = tunnel_parser
154
166
  parser.reset
155
- if @pending.first.is_a?(ConnectRequest)
167
+ if @pending.first.is_a?(@options.connect_request_class)
156
168
  connect_request = @pending.shift # this happened when reenqueing
157
169
  end
158
170
  end
@@ -160,10 +172,7 @@ module HTTPX
160
172
  idling
161
173
 
162
174
  @tunnel_parser = @parser = parser
163
- if connect_request
164
- @inflight += 1
165
- parser.send(connect_request)
166
- end
175
+ parser.send(connect_request) if connect_request
167
176
  transition(:connecting)
168
177
  end
169
178
  end
@@ -197,7 +206,7 @@ module HTTPX
197
206
  # all requests in the queue are not only ALL HTTPS, but they also share the certificate,
198
207
  # and therefore, will share the connection.
199
208
  #
200
- connect_request = ConnectRequest.new(req.uri, @options)
209
+ connect_request = @options.connect_request_class.new(req.uri, @options)
201
210
  send(connect_request)
202
211
  else
203
212
  handle_transition(:connected)
@@ -205,7 +214,6 @@ module HTTPX
205
214
  end
206
215
 
207
216
  def __http_on_connect(request, response)
208
- @inflight -= 1
209
217
  if response.is_a?(Response) && response.status == 200
210
218
  req = @pending.first
211
219
  request_uri = req.uri
@@ -221,8 +229,7 @@ module HTTPX
221
229
  request.headers["proxy-authorization"] = @options.proxy.authenticate(request, response.headers["proxy-authenticate"])
222
230
  send(request)
223
231
  else
224
- pending = @pending + @parser.pending
225
- while (req = pending.shift)
232
+ while (req = @pending.shift)
226
233
  response.finish!
227
234
  req.response = response
228
235
  req.emit_response(response)
@@ -252,7 +259,7 @@ module HTTPX
252
259
  end
253
260
  end
254
261
 
255
- class ConnectRequest < Request
262
+ module ConnectRequestMethods
256
263
  def initialize(uri, options)
257
264
  super("CONNECT", uri, options)
258
265
  @headers = options.headers_class.new
@@ -42,10 +42,6 @@ module HTTPX
42
42
  end
43
43
  end
44
44
 
45
- def connecting?
46
- super || @state == :authenticating || @state == :negotiating
47
- end
48
-
49
45
  def interests
50
46
  if @state == :connecting || @state == :authenticating || @state == :negotiating
51
47
  return @write_buffer.empty? ? :r : :w
@@ -56,6 +52,10 @@ module HTTPX
56
52
 
57
53
  private
58
54
 
55
+ def proxy_connecting?
56
+ super || @state == :authenticating || @state == :negotiating
57
+ end
58
+
59
59
  def handle_transition(nextstate)
60
60
  return super unless @options.proxy && @options.proxy.uri.scheme == "socks5"
61
61
 
@@ -11,7 +11,7 @@ module HTTPX
11
11
  #
12
12
  # * HTTP proxies
13
13
  # * HTTPS proxies
14
- # * Socks4 /Socks4a proxies
14
+ # * Socks4 proxies (also supports Socks4a)
15
15
  # * Socks5 proxies
16
16
  #
17
17
  # https://gitlab.com/os85/httpx/wikis/Proxy
@@ -157,7 +157,7 @@ module HTTPX
157
157
  # :username :: the username to use when authenticating with the proxy
158
158
  # :password :: the password to use when authenticating with the proxy
159
159
  # :no_proxy :: A list of hostnames (or regex patterns) for which the proxy should be bypassed
160
- # (ex: <tt>["example.com", "localhost", /.*\.google\.com/]</tt>).
160
+ # (ex: <tt>["example.com", "localhost", /.*\.google\.com/]</tt>). Can also receive ["*"] as a catch-all.
161
161
  #
162
162
  # if you systems setting environment variables like `HTTP_PROXY` or `NO_PROXY` though, you won't need to set
163
163
  # most of the above.
@@ -287,7 +287,7 @@ module HTTPX
287
287
  def connecting?
288
288
  return super unless @options.proxy
289
289
 
290
- super || @state == :connecting || @state == :connected
290
+ super || proxy_connecting?
291
291
  end
292
292
 
293
293
  def call
@@ -309,15 +309,6 @@ module HTTPX
309
309
  raise e
310
310
  end
311
311
 
312
- def reset
313
- return super unless @options.proxy
314
-
315
- @state = :open
316
-
317
- super
318
- # emit(:close)
319
- end
320
-
321
312
  private
322
313
 
323
314
  def initialize_type(uri, options)
@@ -326,6 +317,10 @@ module HTTPX
326
317
  "tcp"
327
318
  end
328
319
 
320
+ def proxy_connecting?
321
+ @state == :connecting || @state == :connected
322
+ end
323
+
329
324
  def connect
330
325
  return super unless @options.proxy
331
326
 
@@ -342,9 +337,7 @@ module HTTPX
342
337
 
343
338
  case nextstate
344
339
  when :closing
345
- # this is a hack so that we can use the super method
346
- # and it'll think that the current state is open
347
- @state = :open if @state == :connecting
340
+ @parser = nil if proxy_connecting?
348
341
  end
349
342
  super
350
343
  end
@@ -14,6 +14,7 @@ module HTTPX
14
14
  end
15
15
 
16
16
  module InstanceMethods
17
+ # sends a QUERY request for the given +uri+, using +options+ for each.
17
18
  def query(*uri, **options)
18
19
  request("QUERY", uri, **options)
19
20
  end
@@ -125,12 +125,12 @@ module HTTPX
125
125
  # https://gitlab.com/os85/httpx/wikis/Stream
126
126
  #
127
127
  module Stream
128
- STREAM_REQUEST_OPTIONS = { timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 }.freeze }.freeze
128
+ STREAM_REQUEST_OPTIONS = { timeout: { operation_timeout: 60 }.freeze }.freeze
129
129
 
130
130
  def self.extra_options(options)
131
131
  options.merge(
132
132
  stream: false,
133
- timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 },
133
+ timeout: { operation_timeout: 60 },
134
134
  stream_response_class: Class.new(StreamResponse, &Options::SET_TEMPORARY_NAME).freeze
135
135
  )
136
136
  end
@@ -140,14 +140,6 @@ module HTTPX
140
140
  # :stream :: whether the request to process should be handled as a stream (defaults to <tt>false</tt>).
141
141
  # :stream_response_class :: Class used to build the stream response object.
142
142
  module OptionsMethods
143
- def option_stream(val)
144
- val
145
- end
146
-
147
- def option_stream_response_class(value)
148
- value
149
- end
150
-
151
143
  def extend_with_plugin_classes(pl)
152
144
  return super unless defined?(pl::StreamResponseMethods)
153
145
 
@@ -157,6 +149,16 @@ module HTTPX
157
149
 
158
150
  super
159
151
  end
152
+
153
+ private
154
+
155
+ def option_stream(val)
156
+ val
157
+ end
158
+
159
+ def option_stream_response_class(value)
160
+ value
161
+ end
160
162
  end
161
163
 
162
164
  module InstanceMethods
@@ -158,6 +158,10 @@ module HTTPX
158
158
  @closed ? :closed : :open
159
159
  end
160
160
 
161
+ def closed_for_select?
162
+ @closed
163
+ end
164
+
161
165
  # noop
162
166
  def log(**, &_); end
163
167