httpx 0.6.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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