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.
- 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
|