httpx 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/LICENSE.txt +48 -0
- data/README.md +9 -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_10_0.md +66 -0
- data/doc/release_notes/0_10_1.md +39 -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.rb +2 -0
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/chainable.rb +11 -11
- data/lib/httpx/connection.rb +23 -31
- data/lib/httpx/connection/http1.rb +30 -4
- data/lib/httpx/connection/http2.rb +29 -10
- data/lib/httpx/domain_name.rb +440 -0
- data/lib/httpx/errors.rb +2 -1
- data/lib/httpx/extensions.rb +22 -2
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +0 -1
- data/lib/httpx/io/tcp.rb +6 -5
- data/lib/httpx/io/udp.rb +4 -1
- data/lib/httpx/options.rb +5 -1
- data/lib/httpx/parser/http1.rb +14 -17
- data/lib/httpx/plugins/compression.rb +46 -65
- data/lib/httpx/plugins/compression/brotli.rb +10 -14
- data/lib/httpx/plugins/compression/deflate.rb +7 -6
- data/lib/httpx/plugins/compression/gzip.rb +23 -5
- data/lib/httpx/plugins/cookies.rb +21 -60
- data/lib/httpx/plugins/cookies/cookie.rb +173 -0
- data/lib/httpx/plugins/cookies/jar.rb +74 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
- data/lib/httpx/plugins/expect.rb +12 -1
- data/lib/httpx/plugins/follow_redirects.rb +20 -2
- data/lib/httpx/plugins/h2c.rb +1 -1
- data/lib/httpx/plugins/multipart.rb +12 -6
- data/lib/httpx/plugins/persistent.rb +6 -1
- data/lib/httpx/plugins/proxy.rb +16 -2
- data/lib/httpx/plugins/proxy/socks4.rb +14 -14
- data/lib/httpx/plugins/rate_limiter.rb +51 -0
- data/lib/httpx/plugins/retries.rb +3 -2
- data/lib/httpx/plugins/stream.rb +109 -13
- data/lib/httpx/pool.rb +14 -17
- data/lib/httpx/request.rb +8 -20
- data/lib/httpx/resolver.rb +7 -10
- data/lib/httpx/resolver/https.rb +22 -24
- data/lib/httpx/resolver/native.rb +19 -16
- data/lib/httpx/resolver/resolver_mixin.rb +4 -2
- data/lib/httpx/resolver/system.rb +2 -2
- data/lib/httpx/response.rb +16 -25
- data/lib/httpx/selector.rb +11 -18
- data/lib/httpx/session.rb +40 -26
- data/lib/httpx/transcoder.rb +18 -0
- data/lib/httpx/transcoder/chunker.rb +0 -2
- data/lib/httpx/transcoder/form.rb +9 -7
- data/lib/httpx/transcoder/json.rb +0 -4
- data/lib/httpx/utils.rb +45 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +24 -0
- data/sig/callbacks.rbs +14 -0
- data/sig/chainable.rbs +37 -0
- data/sig/connection.rbs +85 -0
- data/sig/connection/http1.rbs +66 -0
- data/sig/connection/http2.rbs +78 -0
- data/sig/domain_name.rbs +17 -0
- data/sig/errors.rbs +3 -0
- data/sig/headers.rbs +42 -0
- data/sig/httpx.rbs +15 -0
- data/sig/loggable.rbs +11 -0
- data/sig/missing.rbs +12 -0
- data/sig/options.rbs +118 -0
- data/sig/parser/http1.rbs +50 -0
- data/sig/plugins/authentication.rbs +11 -0
- data/sig/plugins/basic_authentication.rbs +13 -0
- data/sig/plugins/compression.rbs +55 -0
- data/sig/plugins/compression/brotli.rbs +21 -0
- data/sig/plugins/compression/deflate.rbs +17 -0
- data/sig/plugins/compression/gzip.rbs +29 -0
- data/sig/plugins/cookies.rbs +26 -0
- data/sig/plugins/cookies/cookie.rbs +50 -0
- data/sig/plugins/cookies/jar.rbs +27 -0
- data/sig/plugins/digest_authentication.rbs +33 -0
- data/sig/plugins/expect.rbs +19 -0
- data/sig/plugins/follow_redirects.rbs +37 -0
- data/sig/plugins/h2c.rbs +26 -0
- data/sig/plugins/multipart.rbs +21 -0
- data/sig/plugins/persistent.rbs +17 -0
- data/sig/plugins/proxy.rbs +47 -0
- data/sig/plugins/proxy/http.rbs +14 -0
- data/sig/plugins/proxy/socks4.rbs +33 -0
- data/sig/plugins/proxy/socks5.rbs +36 -0
- data/sig/plugins/proxy/ssh.rbs +18 -0
- data/sig/plugins/push_promise.rbs +22 -0
- data/sig/plugins/rate_limiter.rbs +11 -0
- data/sig/plugins/retries.rbs +48 -0
- data/sig/plugins/stream.rbs +39 -0
- data/sig/pool.rbs +36 -0
- data/sig/registry.rbs +9 -0
- data/sig/request.rbs +61 -0
- data/sig/resolver.rbs +26 -0
- data/sig/resolver/https.rbs +49 -0
- data/sig/resolver/native.rbs +60 -0
- data/sig/resolver/resolver_mixin.rbs +27 -0
- data/sig/resolver/system.rbs +17 -0
- data/sig/response.rbs +87 -0
- data/sig/selector.rbs +20 -0
- data/sig/session.rbs +49 -0
- data/sig/timeout.rbs +29 -0
- data/sig/transcoder.rbs +18 -0
- data/sig/transcoder/body.rbs +18 -0
- data/sig/transcoder/chunker.rbs +32 -0
- data/sig/transcoder/form.rbs +16 -0
- data/sig/transcoder/json.rbs +14 -0
- metadata +128 -22
- data/lib/httpx/resolver/options.rb +0 -25
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module HTTPX
|
|
7
|
+
module Plugins::Cookies
|
|
8
|
+
module SetCookieParser
|
|
9
|
+
using(RegexpExtensions) unless Regexp.method_defined?(:match?)
|
|
10
|
+
|
|
11
|
+
# Whitespace.
|
|
12
|
+
RE_WSP = /[ \t]+/.freeze
|
|
13
|
+
|
|
14
|
+
# A pattern that matches a cookie name or attribute name which may
|
|
15
|
+
# be empty, capturing trailing whitespace.
|
|
16
|
+
RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/.freeze
|
|
17
|
+
|
|
18
|
+
RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/.freeze
|
|
19
|
+
|
|
20
|
+
# A pattern that matches the comma in a (typically date) value.
|
|
21
|
+
RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def scan_dquoted(scanner)
|
|
26
|
+
s = +""
|
|
27
|
+
|
|
28
|
+
until scanner.eos?
|
|
29
|
+
break if scanner.skip(/"/)
|
|
30
|
+
|
|
31
|
+
if scanner.skip(/\\/)
|
|
32
|
+
s << scanner.getch
|
|
33
|
+
elsif scanner.scan(/[^"\\]+/)
|
|
34
|
+
s << scanner.matched
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def scan_value(scanner, comma_as_separator = false)
|
|
42
|
+
value = +""
|
|
43
|
+
|
|
44
|
+
until scanner.eos?
|
|
45
|
+
if scanner.scan(/[^,;"]+/)
|
|
46
|
+
value << scanner.matched
|
|
47
|
+
elsif scanner.skip(/"/)
|
|
48
|
+
# RFC 6265 2.2
|
|
49
|
+
# A cookie-value may be DQUOTE'd.
|
|
50
|
+
value << scan_dquoted(scanner)
|
|
51
|
+
elsif scanner.check(/;/)
|
|
52
|
+
break
|
|
53
|
+
elsif comma_as_separator && scanner.check(RE_COOKIE_COMMA)
|
|
54
|
+
break
|
|
55
|
+
else
|
|
56
|
+
value << scanner.getch
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
value.rstrip!
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scan_name_value(scanner, comma_as_separator = false)
|
|
65
|
+
name = scanner.scan(RE_NAME)
|
|
66
|
+
name.rstrip! if name
|
|
67
|
+
|
|
68
|
+
if scanner.skip(/=/)
|
|
69
|
+
value = scan_value(scanner, comma_as_separator)
|
|
70
|
+
else
|
|
71
|
+
scan_value(scanner, comma_as_separator)
|
|
72
|
+
value = nil
|
|
73
|
+
end
|
|
74
|
+
[name, value]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def call(set_cookie)
|
|
78
|
+
scanner = StringScanner.new(set_cookie)
|
|
79
|
+
|
|
80
|
+
# RFC 6265 4.1.1 & 5.2
|
|
81
|
+
until scanner.eos?
|
|
82
|
+
start = scanner.pos
|
|
83
|
+
len = nil
|
|
84
|
+
|
|
85
|
+
scanner.skip(RE_WSP)
|
|
86
|
+
|
|
87
|
+
name, value = scan_name_value(scanner, true)
|
|
88
|
+
value = nil if name.empty?
|
|
89
|
+
|
|
90
|
+
attrs = {}
|
|
91
|
+
|
|
92
|
+
until scanner.eos?
|
|
93
|
+
if scanner.skip(/,/)
|
|
94
|
+
# The comma is used as separator for concatenating multiple
|
|
95
|
+
# values of a header.
|
|
96
|
+
len = (scanner.pos - 1) - start
|
|
97
|
+
break
|
|
98
|
+
elsif scanner.skip(/;/)
|
|
99
|
+
scanner.skip(RE_WSP)
|
|
100
|
+
|
|
101
|
+
aname, avalue = scan_name_value(scanner, true)
|
|
102
|
+
|
|
103
|
+
next if aname.empty? || value.nil?
|
|
104
|
+
|
|
105
|
+
aname.downcase!
|
|
106
|
+
|
|
107
|
+
case aname
|
|
108
|
+
when "expires"
|
|
109
|
+
# RFC 6265 5.2.1
|
|
110
|
+
(avalue &&= Time.httpdate(avalue)) || next
|
|
111
|
+
when "max-age"
|
|
112
|
+
# RFC 6265 5.2.2
|
|
113
|
+
next unless /\A-?\d+\z/.match?(avalue)
|
|
114
|
+
|
|
115
|
+
avalue = Integer(avalue)
|
|
116
|
+
when "domain"
|
|
117
|
+
# RFC 6265 5.2.3
|
|
118
|
+
# An empty value SHOULD be ignored.
|
|
119
|
+
next if avalue.nil? || avalue.empty?
|
|
120
|
+
when "path"
|
|
121
|
+
# RFC 6265 5.2.4
|
|
122
|
+
# A relative path must be ignored rather than normalizing it
|
|
123
|
+
# to "/".
|
|
124
|
+
next unless avalue.start_with?("/")
|
|
125
|
+
when "secure", "httponly"
|
|
126
|
+
# RFC 6265 5.2.5, 5.2.6
|
|
127
|
+
avalue = true
|
|
128
|
+
end
|
|
129
|
+
attrs[aname] = avalue
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
len ||= scanner.pos - start
|
|
134
|
+
|
|
135
|
+
next if len > Cookie::MAX_LENGTH
|
|
136
|
+
|
|
137
|
+
yield(name, value, attrs) if name && !name.empty? && value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/httpx/plugins/expect.rb
CHANGED
|
@@ -18,14 +18,24 @@ module HTTPX
|
|
|
18
18
|
|
|
19
19
|
seconds
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def_option(:expect_threshold_size) do |bytes|
|
|
23
|
+
bytes = Integer(bytes)
|
|
24
|
+
raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
|
|
25
|
+
|
|
26
|
+
bytes
|
|
27
|
+
end
|
|
21
28
|
end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
module RequestBodyMethods
|
|
25
|
-
def initialize(
|
|
32
|
+
def initialize(*, options)
|
|
26
33
|
super
|
|
27
34
|
return if @body.nil?
|
|
28
35
|
|
|
36
|
+
threshold = options.expect_threshold_size
|
|
37
|
+
return if threshold && !unbounded_body? && @body.bytesize < threshold
|
|
38
|
+
|
|
29
39
|
@headers["expect"] = "100-continue"
|
|
30
40
|
end
|
|
31
41
|
end
|
|
@@ -50,6 +60,7 @@ module HTTPX
|
|
|
50
60
|
return unless response
|
|
51
61
|
|
|
52
62
|
if response.status == 417 && request.headers.key?("expect")
|
|
63
|
+
response.close
|
|
53
64
|
request.headers.delete("expect")
|
|
54
65
|
request.transition(:idle)
|
|
55
66
|
connection = find_connection(request, connections, options)
|
|
@@ -59,8 +59,26 @@ module HTTPX
|
|
|
59
59
|
return ErrorResponse.new(request, error, options)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
retry_after = response.headers["retry-after"]
|
|
63
|
+
|
|
64
|
+
if retry_after
|
|
65
|
+
# Servers send the "Retry-After" header field to indicate how long the
|
|
66
|
+
# user agent ought to wait before making a follow-up request.
|
|
67
|
+
# When sent with any 3xx (Redirection) response, Retry-After indicates
|
|
68
|
+
# the minimum time that the user agent is asked to wait before issuing
|
|
69
|
+
# the redirected request.
|
|
70
|
+
#
|
|
71
|
+
retry_after = Utils.parse_retry_after(retry_after)
|
|
72
|
+
|
|
73
|
+
log { "redirecting after #{retry_after} secs..." }
|
|
74
|
+
pool.after(retry_after) do
|
|
75
|
+
connection = find_connection(retry_request, connections, options)
|
|
76
|
+
connection.send(retry_request)
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
connection = find_connection(retry_request, connections, options)
|
|
80
|
+
connection.send(retry_request)
|
|
81
|
+
end
|
|
64
82
|
nil
|
|
65
83
|
end
|
|
66
84
|
|
data/lib/httpx/plugins/h2c.rb
CHANGED
|
@@ -23,19 +23,25 @@ module HTTPX
|
|
|
23
23
|
def_delegator :@raw, :read
|
|
24
24
|
|
|
25
25
|
def initialize(form)
|
|
26
|
-
@raw =
|
|
26
|
+
@raw = if multipart?(form)
|
|
27
|
+
HTTP::FormData::Multipart.new(Hash[*form.map { |k, v| Transcoder.enum_for(:normalize_keys, k, v).to_a }])
|
|
28
|
+
else
|
|
29
|
+
HTTP::FormData::Urlencoded.new(form, :encoder => Transcoder::Form.method(:encode))
|
|
30
|
+
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def bytesize
|
|
30
34
|
@raw.content_length
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
@raw.to_s.force_encoding(*args)
|
|
35
|
-
end
|
|
37
|
+
private
|
|
36
38
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
+
def multipart?(data)
|
|
40
|
+
data.any? do |_, v|
|
|
41
|
+
v.is_a?(HTTP::FormData::Part) ||
|
|
42
|
+
(v.respond_to?(:to_ary) && v.to_ary.any? { |e| e.is_a?(HTTP::FormData::Part) }) ||
|
|
43
|
+
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| e.is_a?(HTTP::FormData::Part) })
|
|
44
|
+
end
|
|
39
45
|
end
|
|
40
46
|
end
|
|
41
47
|
|
|
@@ -19,7 +19,12 @@ module HTTPX
|
|
|
19
19
|
#
|
|
20
20
|
module Persistent
|
|
21
21
|
def self.load_dependencies(klass)
|
|
22
|
-
klass.
|
|
22
|
+
max_retries = if klass.default_options.respond_to?(:max_retries)
|
|
23
|
+
[klass.default_options.max_retries, 1].max
|
|
24
|
+
else
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
klass.plugin(:retries, max_retries: max_retries, retry_change_requests: true)
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def self.extra_options(options)
|
data/lib/httpx/plugins/proxy.rb
CHANGED
|
@@ -121,8 +121,7 @@ module HTTPX
|
|
|
121
121
|
def fetch_response(request, connections, options)
|
|
122
122
|
response = super
|
|
123
123
|
if response.is_a?(ErrorResponse) &&
|
|
124
|
-
|
|
125
|
-
PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
|
|
124
|
+
__proxy_error?(response) && !@_proxy_uris.empty?
|
|
126
125
|
@_proxy_uris.shift
|
|
127
126
|
log { "failed connecting to proxy, trying next..." }
|
|
128
127
|
request.transition(:idle)
|
|
@@ -139,6 +138,21 @@ module HTTPX
|
|
|
139
138
|
|
|
140
139
|
super
|
|
141
140
|
end
|
|
141
|
+
|
|
142
|
+
def __proxy_error?(response)
|
|
143
|
+
error = response.error
|
|
144
|
+
case error
|
|
145
|
+
when ResolveError
|
|
146
|
+
# failed resolving proxy domain
|
|
147
|
+
proxy_uri = error.connection.options.proxy.uri
|
|
148
|
+
proxy_uri.to_s == @_proxy_uris.first
|
|
149
|
+
when *PROXY_ERRORS
|
|
150
|
+
# timeout errors connecting to proxy
|
|
151
|
+
true
|
|
152
|
+
else
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
142
156
|
end
|
|
143
157
|
|
|
144
158
|
module ConnectionMethods
|
|
@@ -10,7 +10,7 @@ module HTTPX
|
|
|
10
10
|
module Socks4
|
|
11
11
|
VERSION = 4
|
|
12
12
|
CONNECT = 1
|
|
13
|
-
GRANTED =
|
|
13
|
+
GRANTED = 0x5A
|
|
14
14
|
PROTOCOLS = %w[socks4 socks4a].freeze
|
|
15
15
|
|
|
16
16
|
Error = Socks4Error
|
|
@@ -95,21 +95,21 @@ module HTTPX
|
|
|
95
95
|
|
|
96
96
|
def connect(parameters, uri)
|
|
97
97
|
packet = [VERSION, CONNECT, uri.port].pack("CCn")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else
|
|
109
|
-
packet << "\x0\x0\x0\x1" << "\x7\x0" << uri.host
|
|
98
|
+
|
|
99
|
+
case parameters.uri.scheme
|
|
100
|
+
when "socks4"
|
|
101
|
+
socks_host = uri.host
|
|
102
|
+
begin
|
|
103
|
+
ip = IPAddr.new(socks_host)
|
|
104
|
+
packet << ip.hton
|
|
105
|
+
rescue IPAddr::InvalidAddressError
|
|
106
|
+
socks_host = Resolv.getaddress(socks_host)
|
|
107
|
+
retry
|
|
110
108
|
end
|
|
109
|
+
packet << [parameters.username].pack("Z*")
|
|
110
|
+
when "socks4a"
|
|
111
|
+
packet << "\x0\x0\x0\x1" << [parameters.username].pack("Z*") << uri.host << "\x0"
|
|
111
112
|
end
|
|
112
|
-
packet << [parameters.username].pack("Z*")
|
|
113
113
|
packet
|
|
114
114
|
end
|
|
115
115
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for retrying requests when the request:
|
|
7
|
+
#
|
|
8
|
+
# * is rate limited;
|
|
9
|
+
# * when the server is unavailable (503);
|
|
10
|
+
# * when a 3xx request comes with a "retry-after" value
|
|
11
|
+
#
|
|
12
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/RateLimiter
|
|
13
|
+
#
|
|
14
|
+
module RateLimiter
|
|
15
|
+
class << self
|
|
16
|
+
RATE_LIMIT_CODES = [429, 503].freeze
|
|
17
|
+
|
|
18
|
+
def load_dependencies(klass)
|
|
19
|
+
klass.plugin(:retries,
|
|
20
|
+
retry_change_requests: true,
|
|
21
|
+
retry_on: method(:retry_on_rate_limited_response),
|
|
22
|
+
retry_after: method(:retry_after_rate_limit))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retry_on_rate_limited_response(response)
|
|
26
|
+
status = response.status
|
|
27
|
+
|
|
28
|
+
RATE_LIMIT_CODES.include?(status)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Servers send the "Retry-After" header field to indicate how long the
|
|
32
|
+
# user agent ought to wait before making a follow-up request. When
|
|
33
|
+
# sent with a 503 (Service Unavailable) response, Retry-After indicates
|
|
34
|
+
# how long the service is expected to be unavailable to the client.
|
|
35
|
+
# When sent with any 3xx (Redirection) response, Retry-After indicates
|
|
36
|
+
# the minimum time that the user agent is asked to wait before issuing
|
|
37
|
+
# the redirected request.
|
|
38
|
+
#
|
|
39
|
+
def retry_after_rate_limit(_, response)
|
|
40
|
+
retry_after = response.headers["retry-after"]
|
|
41
|
+
|
|
42
|
+
return unless retry_after
|
|
43
|
+
|
|
44
|
+
Utils.parse_retry_after(retry_after)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
register_plugin :rate_limiter, RateLimiter
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -75,14 +75,15 @@ module HTTPX
|
|
|
75
75
|
)
|
|
76
76
|
# rubocop:enable Style/MultilineTernaryOperator
|
|
77
77
|
)
|
|
78
|
-
|
|
78
|
+
response.close if response.respond_to?(:close)
|
|
79
79
|
request.retries -= 1
|
|
80
80
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
81
81
|
request.transition(:idle)
|
|
82
82
|
|
|
83
83
|
retry_after = options.retry_after
|
|
84
|
+
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
85
|
+
|
|
84
86
|
if retry_after
|
|
85
|
-
retry_after = retry_after.call(request) if retry_after.respond_to?(:call)
|
|
86
87
|
|
|
87
88
|
log { "retrying after #{retry_after} secs..." }
|
|
88
89
|
pool.after(retry_after) do
|
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -7,27 +7,123 @@ module HTTPX
|
|
|
7
7
|
#
|
|
8
8
|
module Stream
|
|
9
9
|
module InstanceMethods
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def request(*args, stream: false, **options)
|
|
13
|
+
return super(*args, **options) unless stream
|
|
14
|
+
|
|
15
|
+
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
|
16
|
+
|
|
17
|
+
raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
|
|
18
|
+
|
|
19
|
+
StreamResponse.new(requests.first, self)
|
|
13
20
|
end
|
|
14
21
|
end
|
|
15
22
|
|
|
23
|
+
module RequestMethods
|
|
24
|
+
attr_accessor :stream
|
|
25
|
+
end
|
|
26
|
+
|
|
16
27
|
module ResponseMethods
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
def stream
|
|
29
|
+
@request.stream
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module ResponseBodyMethods
|
|
34
|
+
def initialize(*, **)
|
|
35
|
+
super
|
|
36
|
+
@stream = @response.stream
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write(chunk)
|
|
40
|
+
return super unless @stream
|
|
41
|
+
|
|
42
|
+
@stream.on_chunk(chunk.to_s.dup)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def transition(*)
|
|
48
|
+
return if @stream
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class StreamResponse
|
|
55
|
+
def initialize(request, session)
|
|
56
|
+
@request = request
|
|
57
|
+
@session = session
|
|
58
|
+
@options = @request.options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each(&block)
|
|
62
|
+
return enum_for(__method__) unless block_given?
|
|
63
|
+
|
|
64
|
+
raise Error, "response already streamed" if @response
|
|
65
|
+
|
|
66
|
+
@request.stream = self
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
@on_chunk = block
|
|
70
|
+
|
|
71
|
+
response.raise_for_status
|
|
72
|
+
response.close
|
|
73
|
+
ensure
|
|
74
|
+
@on_chunk = nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def each_line
|
|
79
|
+
return enum_for(__method__) unless block_given?
|
|
80
|
+
|
|
81
|
+
line = +""
|
|
82
|
+
|
|
83
|
+
each do |chunk|
|
|
84
|
+
line << chunk
|
|
85
|
+
|
|
86
|
+
while (idx = line.index("\n"))
|
|
87
|
+
yield line.byteslice(0..idx - 1)
|
|
88
|
+
|
|
89
|
+
line = line.byteslice(idx + 1..-1)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# This is a ghost method. It's to be used ONLY internally, when processing streams
|
|
95
|
+
def on_chunk(chunk)
|
|
96
|
+
raise NoMethodError unless @on_chunk
|
|
97
|
+
|
|
98
|
+
@on_chunk.call(chunk)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :nocov:
|
|
102
|
+
def inspect
|
|
103
|
+
"#<StreamResponse:#{object_id}>"
|
|
104
|
+
end
|
|
105
|
+
# :nocov:
|
|
106
|
+
|
|
107
|
+
def to_s
|
|
108
|
+
response.to_s
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def response
|
|
114
|
+
@response ||= @session.__send__(:send_requests, @request, @options).first
|
|
21
115
|
end
|
|
22
116
|
|
|
23
|
-
def
|
|
24
|
-
@
|
|
117
|
+
def respond_to_missing?(*args)
|
|
118
|
+
@options.response_class.respond_to?(*args) || super
|
|
25
119
|
end
|
|
26
120
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
121
|
+
def method_missing(meth, *args, &block)
|
|
122
|
+
if @options.response_class.public_method_defined?(meth)
|
|
123
|
+
response.__send__(meth, *args, &block)
|
|
124
|
+
else
|
|
125
|
+
super
|
|
126
|
+
end
|
|
31
127
|
end
|
|
32
128
|
end
|
|
33
129
|
end
|