httpx 0.6.7 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -5
- data/doc/release_notes/0_0_1.md +7 -0
- data/doc/release_notes/0_0_2.md +9 -0
- data/doc/release_notes/0_0_3.md +9 -0
- data/doc/release_notes/0_0_4.md +7 -0
- data/doc/release_notes/0_0_5.md +5 -0
- data/doc/release_notes/0_1_0.md +9 -0
- data/doc/release_notes/0_2_0.md +5 -0
- data/doc/release_notes/0_2_1.md +16 -0
- data/doc/release_notes/0_3_0.md +12 -0
- data/doc/release_notes/0_3_1.md +6 -0
- data/doc/release_notes/0_4_0.md +51 -0
- data/doc/release_notes/0_4_1.md +3 -0
- data/doc/release_notes/0_5_0.md +15 -0
- data/doc/release_notes/0_5_1.md +14 -0
- data/doc/release_notes/0_6_0.md +5 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_6_2.md +6 -0
- data/doc/release_notes/0_6_3.md +13 -0
- data/doc/release_notes/0_6_4.md +21 -0
- data/doc/release_notes/0_6_5.md +22 -0
- data/doc/release_notes/0_6_6.md +19 -0
- data/doc/release_notes/0_6_7.md +5 -0
- data/doc/release_notes/0_7_0.md +46 -0
- data/doc/release_notes/0_8_0.md +27 -0
- data/doc/release_notes/0_8_1.md +8 -0
- data/doc/release_notes/0_8_2.md +7 -0
- data/doc/release_notes/0_9_0.md +38 -0
- data/lib/httpx/adapters/faraday.rb +2 -2
- data/lib/httpx/altsvc.rb +18 -2
- data/lib/httpx/chainable.rb +27 -9
- data/lib/httpx/connection.rb +215 -65
- data/lib/httpx/connection/http1.rb +54 -18
- data/lib/httpx/connection/http2.rb +100 -37
- data/lib/httpx/extensions.rb +2 -2
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +11 -3
- data/lib/httpx/io/tcp.rb +12 -2
- data/lib/httpx/loggable.rb +6 -6
- data/lib/httpx/options.rb +43 -28
- data/lib/httpx/plugins/authentication.rb +1 -1
- data/lib/httpx/plugins/compression.rb +28 -8
- data/lib/httpx/plugins/compression/gzip.rb +22 -12
- data/lib/httpx/plugins/cookies.rb +12 -8
- data/lib/httpx/plugins/digest_authentication.rb +2 -0
- data/lib/httpx/plugins/expect.rb +79 -0
- data/lib/httpx/plugins/follow_redirects.rb +1 -2
- data/lib/httpx/plugins/h2c.rb +0 -1
- data/lib/httpx/plugins/proxy.rb +23 -20
- data/lib/httpx/plugins/proxy/http.rb +9 -6
- data/lib/httpx/plugins/proxy/socks4.rb +1 -1
- data/lib/httpx/plugins/proxy/socks5.rb +5 -1
- data/lib/httpx/plugins/proxy/ssh.rb +0 -4
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/plugins/retries.rb +32 -29
- data/lib/httpx/pool.rb +15 -10
- data/lib/httpx/registry.rb +2 -1
- data/lib/httpx/request.rb +8 -6
- data/lib/httpx/resolver.rb +7 -8
- data/lib/httpx/resolver/https.rb +15 -3
- data/lib/httpx/resolver/native.rb +22 -32
- data/lib/httpx/resolver/options.rb +2 -2
- data/lib/httpx/resolver/resolver_mixin.rb +1 -1
- data/lib/httpx/response.rb +17 -3
- data/lib/httpx/selector.rb +96 -95
- data/lib/httpx/session.rb +33 -34
- data/lib/httpx/timeout.rb +7 -1
- data/lib/httpx/version.rb +1 -1
- 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 =
|
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
|
-
|
46
|
-
@requests.size >= @max_concurrent_requests
|
64
|
+
unless @max_requests.positive?
|
47
65
|
@pending << request
|
48
66
|
return
|
49
67
|
end
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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.
|
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
|
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
|
31
|
-
|
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
|
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,
|
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,
|
146
|
-
log(level: 2,
|
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(
|
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,
|
173
|
-
log(level: 2,
|
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
|
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
|
-
|
207
|
-
|
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
|
226
|
-
log(level: 2,
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
238
|
-
log(level: 2,
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
250
|
-
log(level: 2
|
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
|
data/lib/httpx/extensions.rb
CHANGED
@@ -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
|
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
|