httpx 0.6.7 → 0.9.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/doc/release_notes/0_0_1.md +7 -0
  4. data/doc/release_notes/0_0_2.md +9 -0
  5. data/doc/release_notes/0_0_3.md +9 -0
  6. data/doc/release_notes/0_0_4.md +7 -0
  7. data/doc/release_notes/0_0_5.md +5 -0
  8. data/doc/release_notes/0_1_0.md +9 -0
  9. data/doc/release_notes/0_2_0.md +5 -0
  10. data/doc/release_notes/0_2_1.md +16 -0
  11. data/doc/release_notes/0_3_0.md +12 -0
  12. data/doc/release_notes/0_3_1.md +6 -0
  13. data/doc/release_notes/0_4_0.md +51 -0
  14. data/doc/release_notes/0_4_1.md +3 -0
  15. data/doc/release_notes/0_5_0.md +15 -0
  16. data/doc/release_notes/0_5_1.md +14 -0
  17. data/doc/release_notes/0_6_0.md +5 -0
  18. data/doc/release_notes/0_6_1.md +6 -0
  19. data/doc/release_notes/0_6_2.md +6 -0
  20. data/doc/release_notes/0_6_3.md +13 -0
  21. data/doc/release_notes/0_6_4.md +21 -0
  22. data/doc/release_notes/0_6_5.md +22 -0
  23. data/doc/release_notes/0_6_6.md +19 -0
  24. data/doc/release_notes/0_6_7.md +5 -0
  25. data/doc/release_notes/0_7_0.md +46 -0
  26. data/doc/release_notes/0_8_0.md +27 -0
  27. data/doc/release_notes/0_8_1.md +8 -0
  28. data/doc/release_notes/0_8_2.md +7 -0
  29. data/doc/release_notes/0_9_0.md +38 -0
  30. data/lib/httpx/adapters/faraday.rb +2 -2
  31. data/lib/httpx/altsvc.rb +18 -2
  32. data/lib/httpx/chainable.rb +27 -9
  33. data/lib/httpx/connection.rb +215 -65
  34. data/lib/httpx/connection/http1.rb +54 -18
  35. data/lib/httpx/connection/http2.rb +100 -37
  36. data/lib/httpx/extensions.rb +2 -2
  37. data/lib/httpx/headers.rb +2 -2
  38. data/lib/httpx/io/ssl.rb +11 -3
  39. data/lib/httpx/io/tcp.rb +12 -2
  40. data/lib/httpx/loggable.rb +6 -6
  41. data/lib/httpx/options.rb +43 -28
  42. data/lib/httpx/plugins/authentication.rb +1 -1
  43. data/lib/httpx/plugins/compression.rb +28 -8
  44. data/lib/httpx/plugins/compression/gzip.rb +22 -12
  45. data/lib/httpx/plugins/cookies.rb +12 -8
  46. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  47. data/lib/httpx/plugins/expect.rb +79 -0
  48. data/lib/httpx/plugins/follow_redirects.rb +1 -2
  49. data/lib/httpx/plugins/h2c.rb +0 -1
  50. data/lib/httpx/plugins/proxy.rb +23 -20
  51. data/lib/httpx/plugins/proxy/http.rb +9 -6
  52. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  53. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  54. data/lib/httpx/plugins/proxy/ssh.rb +0 -4
  55. data/lib/httpx/plugins/push_promise.rb +2 -2
  56. data/lib/httpx/plugins/retries.rb +32 -29
  57. data/lib/httpx/pool.rb +15 -10
  58. data/lib/httpx/registry.rb +2 -1
  59. data/lib/httpx/request.rb +8 -6
  60. data/lib/httpx/resolver.rb +7 -8
  61. data/lib/httpx/resolver/https.rb +15 -3
  62. data/lib/httpx/resolver/native.rb +22 -32
  63. data/lib/httpx/resolver/options.rb +2 -2
  64. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  65. data/lib/httpx/response.rb +17 -3
  66. data/lib/httpx/selector.rb +96 -95
  67. data/lib/httpx/session.rb +33 -34
  68. data/lib/httpx/timeout.rb +7 -1
  69. data/lib/httpx/version.rb +1 -1
  70. metadata +77 -20
@@ -7,14 +7,15 @@ module HTTPX
7
7
  include Callbacks
8
8
  include Loggable
9
9
 
10
+ MAX_REQUESTS = 100
10
11
  CRLF = "\r\n"
11
12
 
12
13
  attr_reader :pending
13
14
 
14
15
  def initialize(buffer, options)
15
16
  @options = Options.new(options)
16
- @max_concurrent_requests = @options.max_concurrent_requests
17
- @max_requests = Float::INFINITY
17
+ @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
18
+ @max_requests = @options.max_requests || MAX_REQUESTS
18
19
  @parser = Parser::HTTP1.new(self)
19
20
  @buffer = buffer
20
21
  @version = [1, 1]
@@ -22,13 +23,31 @@ module HTTPX
22
23
  @requests = []
23
24
  end
24
25
 
26
+ def interests
27
+ # this means we're processing incoming response already
28
+ return :r if @request
29
+
30
+ return if @requests.empty?
31
+
32
+ request = @requests.first
33
+
34
+ return :w if request.interests == :w || !@buffer.empty?
35
+
36
+ :r
37
+ end
38
+
25
39
  def reset
40
+ @max_requests = @options.max_requests || MAX_REQUESTS
26
41
  @parser.reset!
27
42
  end
28
43
 
29
44
  def close
30
45
  reset
31
- emit(:close)
46
+ emit(:close, true)
47
+ end
48
+
49
+ def exhausted?
50
+ !@max_requests.positive?
32
51
  end
33
52
 
34
53
  def empty?
@@ -42,20 +61,24 @@ module HTTPX
42
61
  end
43
62
 
44
63
  def send(request)
45
- if @max_requests.positive? &&
46
- @requests.size >= @max_concurrent_requests
64
+ unless @max_requests.positive?
47
65
  @pending << request
48
66
  return
49
67
  end
50
- unless @requests.include?(request)
51
- @requests << request
52
- @pipelining = true if @requests.size > 1
53
- end
54
- handle(request)
68
+
69
+ return if @requests.include?(request)
70
+
71
+ @requests << request
72
+ @pipelining = true if @requests.size > 1
55
73
  end
56
74
 
57
75
  def consume
58
- @requests.each do |request|
76
+ requests_limit = [@max_concurrent_requests, @max_requests, @requests.size].min
77
+ @requests.each_with_index do |request, idx|
78
+ break if idx >= requests_limit
79
+ next if request.state == :done
80
+
81
+ request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
59
82
  handle(request)
60
83
  end
61
84
  end
@@ -114,7 +137,7 @@ module HTTPX
114
137
 
115
138
  def dispatch
116
139
  if @request.expects?
117
- reset
140
+ @parser.reset!
118
141
  return handle(@request)
119
142
  end
120
143
 
@@ -129,10 +152,10 @@ module HTTPX
129
152
  throw(:called)
130
153
  end
131
154
 
132
- reset
155
+ @parser.reset!
133
156
  @max_requests -= 1
134
- send(@pending.shift) unless @pending.empty?
135
157
  manage_connection(response)
158
+ send(@pending.shift) unless @pending.empty?
136
159
  end
137
160
 
138
161
  def handle_error(ex)
@@ -142,9 +165,17 @@ module HTTPX
142
165
  @requests.each do |request|
143
166
  emit(:error, request, ex)
144
167
  end
168
+ @pending.each do |request|
169
+ emit(:error, request, ex)
170
+ end
145
171
  end
146
172
  end
147
173
 
174
+ def ping
175
+ emit(:reset)
176
+ emit(:exhausted)
177
+ end
178
+
148
179
  private
149
180
 
150
181
  def manage_connection(response)
@@ -163,13 +194,11 @@ module HTTPX
163
194
  emit(:timeout, keep_alive_timeout)
164
195
  end
165
196
  when /close/i
166
- @max_requests = Float::INFINITY
167
197
  disable
168
198
  when nil
169
199
  # In HTTP/1.1, it's keep alive by default
170
200
  return if response.version == "1.1"
171
201
 
172
- @max_requests = Float::INFINITY
173
202
  disable
174
203
  end
175
204
  end
@@ -183,7 +212,14 @@ module HTTPX
183
212
  def disable_pipelining
184
213
  return if @requests.empty?
185
214
 
186
- @requests.each { |r| r.transition(:idle) }
215
+ @requests.each do |r|
216
+ r.transition(:idle)
217
+
218
+ # when we disable pipelining, we still want to try keep-alive.
219
+ # only when keep-alive with one request fails, do we fallback to
220
+ # connection: close.
221
+ r.headers["connection"] = "close" if @max_concurrent_requests == 1
222
+ end
187
223
  # server doesn't handle pipelining, and probably
188
224
  # doesn't support keep-alive. Fallback to send only
189
225
  # 1 keep alive request.
@@ -193,7 +229,7 @@ module HTTPX
193
229
 
194
230
  def set_request_headers(request)
195
231
  request.headers["host"] ||= request.authority
196
- request.headers["connection"] ||= "keep-alive"
232
+ request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
197
233
  if !request.headers.key?("content-length") &&
198
234
  request.body.bytesize == Float::INFINITY
199
235
  request.chunk!
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
3
4
  require "io/wait"
4
5
  require "http/2/next"
5
6
 
@@ -8,6 +9,8 @@ module HTTPX
8
9
  include Callbacks
9
10
  include Loggable
10
11
 
12
+ MAX_CONCURRENT_REQUESTS = HTTP2Next::DEFAULT_MAX_CONCURRENT_STREAMS
13
+
11
14
  Error = Class.new(Error) do
12
15
  def initialize(id, code)
13
16
  super("stream #{id} closed with error: #{code}")
@@ -18,30 +21,63 @@ module HTTPX
18
21
 
19
22
  def initialize(buffer, options)
20
23
  @options = Options.new(options)
21
- @max_concurrent_requests = @options.max_concurrent_requests
24
+ @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
25
+ @max_requests = @options.max_requests || 0
22
26
  @pending = []
23
27
  @streams = {}
24
28
  @drains = {}
29
+ @pings = []
25
30
  @buffer = buffer
26
31
  @handshake_completed = false
27
32
  init_connection
28
33
  end
29
34
 
30
- def close
31
- @connection.goaway
35
+ def interests
36
+ # waiting for WINDOW_UPDATE frames
37
+ return :r if @buffer.full?
38
+
39
+ return :w if @connection.state == :closed
40
+
41
+ unless (@connection.state == :connected && @handshake_completed)
42
+ return @buffer.empty? ? :r : :rw
43
+ end
44
+
45
+ return :w unless @pending.empty?
46
+
47
+ return :w if @streams.each_key.any? { |r| r.interests == :w }
48
+
49
+ return :r if @buffer.empty?
50
+
51
+ :rw
52
+ end
53
+
54
+ def reset
55
+ init_connection
56
+ end
57
+
58
+ def close(*args)
59
+ @connection.goaway(*args) unless @connection.state == :closed
60
+ emit(:close)
32
61
  end
33
62
 
34
63
  def empty?
35
64
  @connection.state == :closed || @streams.empty?
36
65
  end
37
66
 
67
+ def exhausted?
68
+ return false if @max_requests.zero? && @connection.active_stream_count.zero?
69
+
70
+ @connection.active_stream_count >= @max_requests
71
+ end
72
+
38
73
  def <<(data)
39
74
  @connection << data
40
75
  end
41
76
 
42
- def send(request, **)
77
+ def send(request)
43
78
  if !@handshake_completed ||
44
- @streams.size >= @max_concurrent_requests
79
+ @streams.size >= @max_concurrent_requests ||
80
+ @streams.size >= @max_requests
45
81
  @pending << request
46
82
  return
47
83
  end
@@ -49,13 +85,19 @@ module HTTPX
49
85
  stream = @connection.new_stream
50
86
  handle_stream(stream, request)
51
87
  @streams[request] = stream
88
+ @max_requests -= 1
52
89
  end
53
90
  handle(request, stream)
54
91
  true
92
+ rescue HTTP2Next::Error::StreamLimitExceeded
93
+ @pending.unshift(request)
94
+ emit(:exhausted)
55
95
  end
56
96
 
57
97
  def consume
58
98
  @streams.each do |request, stream|
99
+ next if request.state == :done
100
+
59
101
  handle(request, stream)
60
102
  end
61
103
  end
@@ -69,6 +111,13 @@ module HTTPX
69
111
  end
70
112
  end
71
113
 
114
+ def ping
115
+ ping = SecureRandom.gen_random(8)
116
+ @connection.ping(ping)
117
+ ensure
118
+ @pings << ping
119
+ end
120
+
72
121
  private
73
122
 
74
123
  def send_pending
@@ -95,6 +144,7 @@ module HTTPX
95
144
 
96
145
  def init_connection
97
146
  @connection = HTTP2Next::Client.new(@options.http2_settings)
147
+ @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
98
148
  @connection.on(:frame, &method(:on_frame))
99
149
  @connection.on(:frame_sent, &method(:on_frame_sent))
100
150
  @connection.on(:frame_received, &method(:on_frame_received))
@@ -102,6 +152,7 @@ module HTTPX
102
152
  @connection.on(:promise, &method(:on_promise))
103
153
  @connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
104
154
  @connection.on(:settings_ack, &method(:on_settings))
155
+ @connection.on(:ack, &method(:on_pong))
105
156
  @connection.on(:goaway, &method(:on_close))
106
157
  #
107
158
  # Some servers initiate HTTP/2 negotiation right away, some don't.
@@ -115,7 +166,7 @@ module HTTPX
115
166
  def handle_stream(stream, request)
116
167
  stream.on(:close, &method(:on_stream_close).curry[stream, request])
117
168
  stream.on(:half_close) do
118
- log(level: 2, label: "#{stream.id}: ") { "waiting for response..." }
169
+ log(level: 2) { "#{stream.id}: waiting for response..." }
119
170
  end
120
171
  stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
121
172
  stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
@@ -130,8 +181,8 @@ module HTTPX
130
181
  headers[":path"] = headline_uri(request)
131
182
  headers[":authority"] = request.authority
132
183
  headers = headers.merge(request.headers)
133
- log(level: 1, label: "#{stream.id}: ", color: :yellow) do
134
- headers.map { |k, v| "-> HEADER: #{k}: #{v}" }.join("\n")
184
+ log(level: 1, color: :yellow) do
185
+ headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
135
186
  end
136
187
  stream.headers(headers, end_stream: request.empty?)
137
188
  end
@@ -142,8 +193,8 @@ module HTTPX
142
193
  chunk = @drains.delete(request) || request.drain_body
143
194
  while chunk
144
195
  next_chunk = request.drain_body
145
- log(level: 1, label: "#{stream.id}: ", color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
146
- log(level: 2, label: "#{stream.id}: ", color: :green) { "-> #{chunk.inspect}" }
196
+ log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
197
+ log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
147
198
  stream.data(chunk, end_stream: !next_chunk)
148
199
  if next_chunk && @buffer.full?
149
200
  @drains[request] = next_chunk
@@ -158,25 +209,25 @@ module HTTPX
158
209
  ######
159
210
 
160
211
  def on_stream_headers(stream, request, h)
161
- log(label: "#{stream.id}:", color: :yellow) do
162
- h.map { |k, v| "<- HEADER: #{k}: #{v}" }.join("\n")
212
+ log(color: :yellow) do
213
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
163
214
  end
164
215
  _, status = h.shift
165
216
  headers = request.options.headers_class.new(h)
166
217
  response = request.options.response_class.new(request, status, "2.0", headers)
167
218
  request.response = response
168
219
  @streams[request] = stream
220
+
221
+ handle(request, stream) if request.expects?
169
222
  end
170
223
 
171
224
  def on_stream_data(stream, request, data)
172
- log(level: 1, label: "#{stream.id}: ", color: :green) { "<- DATA: #{data.bytesize} bytes..." }
173
- log(level: 2, label: "#{stream.id}: ", color: :green) { "<- #{data.inspect}" }
225
+ log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
226
+ log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
174
227
  request.response << data
175
228
  end
176
229
 
177
230
  def on_stream_close(stream, request, error)
178
- return handle(request, stream) if request.expects?
179
-
180
231
  if error && error != :no_error
181
232
  ex = Error.new(stream.id, error)
182
233
  ex.set_backtrace(caller)
@@ -191,10 +242,14 @@ module HTTPX
191
242
  emit(:response, request, response)
192
243
  end
193
244
  end
194
- log(level: 2, label: "#{stream.id}: ") { "closing stream" }
245
+ log(level: 2) { "#{stream.id}: closing stream" }
195
246
 
196
247
  @streams.delete(request)
197
248
  send(@pending.shift) unless @pending.empty?
249
+ return unless @streams.empty? && exhausted?
250
+
251
+ close
252
+ emit(:exhausted) unless @pending.empty?
198
253
  end
199
254
 
200
255
  def on_frame(bytes)
@@ -203,8 +258,14 @@ module HTTPX
203
258
 
204
259
  def on_settings(*)
205
260
  @handshake_completed = true
206
- @max_concurrent_requests = [@max_concurrent_requests,
207
- @connection.remote_settings[:settings_max_concurrent_streams]].min
261
+
262
+ if @max_requests.zero?
263
+ @max_requests = @connection.remote_settings[:settings_max_concurrent_streams]
264
+
265
+ @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
266
+ end
267
+
268
+ @max_concurrent_requests = [@max_concurrent_requests, @max_requests].min
208
269
  send_pending
209
270
  end
210
271
 
@@ -222,32 +283,26 @@ module HTTPX
222
283
  end
223
284
 
224
285
  def on_frame_sent(frame)
225
- log(level: 2, label: "#{frame[:stream]}: ") { "frame was sent!" }
226
- log(level: 2, label: "#{frame[:stream]}: ", color: :blue) do
227
- case frame[:type]
228
- when :data
229
- frame.merge(payload: frame[:payload].bytesize).inspect
230
- else
231
- frame.inspect
232
- end
286
+ log(level: 2) { "#{frame[:stream]}: frame was sent!" }
287
+ log(level: 2, color: :blue) do
288
+ payload = frame
289
+ payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
290
+ "#{frame[:stream]}: #{payload}"
233
291
  end
234
292
  end
235
293
 
236
294
  def on_frame_received(frame)
237
- log(level: 2, label: "#{frame[:stream]}: ") { "frame was received!" }
238
- log(level: 2, label: "#{frame[:stream]}: ", color: :magenta) do
239
- case frame[:type]
240
- when :data
241
- frame.merge(payload: frame[:payload].bytesize).inspect
242
- else
243
- frame.inspect
244
- end
295
+ log(level: 2) { "#{frame[:stream]}: frame was received!" }
296
+ log(level: 2, color: :magenta) do
297
+ payload = frame
298
+ payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
299
+ "#{frame[:stream]}: #{payload}"
245
300
  end
246
301
  end
247
302
 
248
303
  def on_altsvc(origin, frame)
249
- log(level: 2, label: "#{frame[:stream]}: ") { "altsvc frame was received" }
250
- log(level: 2, label: "#{frame[:stream]}: ") { frame.inspect }
304
+ log(level: 2) { "#{frame[:stream]}: altsvc frame was received" }
305
+ log(level: 2) { "#{frame[:stream]}: #{frame.inspect}" }
251
306
  alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
252
307
  params = { "ma" => frame[:max_age] }
253
308
  emit(:altsvc, origin, alt_origin, origin, params)
@@ -261,6 +316,14 @@ module HTTPX
261
316
  emit(:origin, origin)
262
317
  end
263
318
 
319
+ def on_pong(ping)
320
+ if !@pings.delete(ping)
321
+ close(:protocol_error, "ping payload did not match")
322
+ else
323
+ emit(:pong)
324
+ end
325
+ end
326
+
264
327
  def respond_to_missing?(meth, *args)
265
328
  @connection.respond_to?(meth, *args) || super
266
329
  end
@@ -11,7 +11,7 @@ module HTTPX
11
11
  #
12
12
  # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
13
13
  #
14
- module CurryMethods # :nodoc:
14
+ module CurryMethods
15
15
  # Backport for the Method#curry method, which is part of ruby core since 2.2 .
16
16
  #
17
17
  def curry(*args)
@@ -81,4 +81,4 @@ module HTTPX
81
81
  end
82
82
  end
83
83
  end
84
- end
84
+ end