httpx 0.3.1 → 0.4.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/lib/httpx.rb +8 -2
- data/lib/httpx/adapters/faraday.rb +203 -0
- data/lib/httpx/altsvc.rb +4 -0
- data/lib/httpx/callbacks.rb +1 -4
- data/lib/httpx/chainable.rb +4 -3
- data/lib/httpx/connection.rb +326 -104
- data/lib/httpx/{channel → connection}/http1.rb +29 -15
- data/lib/httpx/{channel → connection}/http2.rb +12 -6
- data/lib/httpx/errors.rb +2 -0
- data/lib/httpx/headers.rb +4 -1
- data/lib/httpx/io/ssl.rb +5 -1
- data/lib/httpx/io/tcp.rb +13 -7
- data/lib/httpx/io/udp.rb +1 -0
- data/lib/httpx/io/unix.rb +1 -0
- data/lib/httpx/loggable.rb +34 -9
- data/lib/httpx/options.rb +57 -31
- data/lib/httpx/parser/http1.rb +8 -0
- data/lib/httpx/plugins/authentication.rb +4 -0
- data/lib/httpx/plugins/basic_authentication.rb +4 -0
- data/lib/httpx/plugins/compression.rb +22 -5
- data/lib/httpx/plugins/cookies.rb +89 -36
- data/lib/httpx/plugins/digest_authentication.rb +45 -26
- data/lib/httpx/plugins/follow_redirects.rb +61 -62
- data/lib/httpx/plugins/h2c.rb +78 -39
- data/lib/httpx/plugins/multipart.rb +5 -0
- data/lib/httpx/plugins/persistent.rb +29 -0
- data/lib/httpx/plugins/proxy.rb +125 -78
- data/lib/httpx/plugins/proxy/http.rb +31 -27
- data/lib/httpx/plugins/proxy/socks4.rb +30 -24
- data/lib/httpx/plugins/proxy/socks5.rb +49 -39
- data/lib/httpx/plugins/proxy/ssh.rb +81 -0
- data/lib/httpx/plugins/push_promise.rb +18 -9
- data/lib/httpx/plugins/retries.rb +43 -15
- data/lib/httpx/pool.rb +159 -0
- data/lib/httpx/registry.rb +2 -0
- data/lib/httpx/request.rb +10 -0
- data/lib/httpx/resolver.rb +2 -1
- data/lib/httpx/resolver/https.rb +62 -56
- data/lib/httpx/resolver/native.rb +48 -37
- data/lib/httpx/resolver/resolver_mixin.rb +16 -11
- data/lib/httpx/resolver/system.rb +11 -7
- data/lib/httpx/response.rb +24 -10
- data/lib/httpx/selector.rb +32 -39
- data/lib/httpx/{client.rb → session.rb} +99 -62
- data/lib/httpx/timeout.rb +7 -15
- data/lib/httpx/transcoder/body.rb +4 -0
- data/lib/httpx/transcoder/chunker.rb +4 -0
- data/lib/httpx/version.rb +1 -1
- metadata +10 -8
- data/lib/httpx/channel.rb +0 -367
data/lib/httpx/plugins/h2c.rb
CHANGED
@@ -2,53 +2,51 @@
|
|
2
2
|
|
3
3
|
module HTTPX
|
4
4
|
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2.
|
7
|
+
#
|
8
|
+
# https://tools.ietf.org/html/rfc7540#section-3.2
|
9
|
+
#
|
5
10
|
module H2C
|
6
11
|
def self.load_dependencies(*)
|
7
12
|
require "base64"
|
8
13
|
end
|
9
14
|
|
10
15
|
module InstanceMethods
|
11
|
-
def request(*args,
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
data = upgrade_response.to_s
|
31
|
-
parser << data
|
32
|
-
response = upgrade_request.response
|
33
|
-
if response.status == 200
|
34
|
-
requests.delete(upgrade_request)
|
35
|
-
return response if requests.empty?
|
36
|
-
end
|
37
|
-
responses = __send_reqs(*requests)
|
38
|
-
else
|
39
|
-
# proceed as usual
|
40
|
-
responses = [upgrade_response] + __send_reqs(*requests[1..-1])
|
41
|
-
end
|
42
|
-
return responses.first if responses.size == 1
|
43
|
-
responses
|
44
|
-
ensure
|
45
|
-
@_h2c_probed = true
|
46
|
-
close unless keep_open
|
47
|
-
end
|
16
|
+
def request(*args, **options)
|
17
|
+
h2c_options = options.merge(fallback_protocol: "h2c")
|
18
|
+
|
19
|
+
requests = build_requests(*args, h2c_options)
|
20
|
+
|
21
|
+
upgrade_request = requests.first
|
22
|
+
return super unless valid_h2c_upgrade_request?(upgrade_request)
|
23
|
+
|
24
|
+
upgrade_request.headers.add("connection", "upgrade")
|
25
|
+
upgrade_request.headers.add("connection", "http2-settings")
|
26
|
+
upgrade_request.headers["upgrade"] = "h2c"
|
27
|
+
upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
|
28
|
+
wrap { send_requests(*upgrade_request, h2c_options).first }
|
29
|
+
|
30
|
+
responses = send_requests(*requests, h2c_options)
|
31
|
+
|
32
|
+
return responses.first if responses.size == 1
|
33
|
+
|
34
|
+
responses
|
48
35
|
end
|
49
36
|
|
50
37
|
private
|
51
38
|
|
39
|
+
def fetch_response(request, connections, options)
|
40
|
+
response = super
|
41
|
+
if response && valid_h2c_upgrade?(request, response, options)
|
42
|
+
log { "upgrading to h2c..." }
|
43
|
+
connection = find_connection(request, connections, options)
|
44
|
+
connections << connection unless connections.include?(connection)
|
45
|
+
connection.upgrade(request, response)
|
46
|
+
end
|
47
|
+
response
|
48
|
+
end
|
49
|
+
|
52
50
|
VALID_H2C_METHODS = %i[get options head].freeze
|
53
51
|
private_constant :VALID_H2C_METHODS
|
54
52
|
|
@@ -56,16 +54,57 @@ module HTTPX
|
|
56
54
|
VALID_H2C_METHODS.include?(request.verb) &&
|
57
55
|
request.scheme == "http"
|
58
56
|
end
|
57
|
+
|
58
|
+
def valid_h2c_upgrade?(request, response, options)
|
59
|
+
options.fallback_protocol == "h2c" &&
|
60
|
+
request.headers.get("connection").include?("upgrade") &&
|
61
|
+
request.headers.get("upgrade").include?("h2c") &&
|
62
|
+
response.status == 101
|
63
|
+
end
|
59
64
|
end
|
60
65
|
|
61
|
-
|
62
|
-
def upgrade(request,
|
66
|
+
class H2CParser < Connection::HTTP2
|
67
|
+
def upgrade(request, response)
|
63
68
|
@connection.send_connection_preface
|
64
69
|
# skip checks, it is assumed that this is the first
|
65
70
|
# request in the connection
|
66
71
|
stream = @connection.upgrade
|
67
72
|
handle_stream(stream, request)
|
68
73
|
@streams[request] = stream
|
74
|
+
|
75
|
+
# clean up data left behind in the buffer, if the server started
|
76
|
+
# sending frames
|
77
|
+
data = response.to_s
|
78
|
+
@connection << data
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module ConnectionMethods
|
83
|
+
using URIExtensions
|
84
|
+
|
85
|
+
def match?(uri, options)
|
86
|
+
return super unless uri.scheme == "http" && @options.fallback_protocol == "h2c"
|
87
|
+
|
88
|
+
super && options.fallback_protocol == "h2c"
|
89
|
+
end
|
90
|
+
|
91
|
+
def coalescable?(connection)
|
92
|
+
return super unless @options.fallback_protocol == "h2c" && @origin.scheme == "http"
|
93
|
+
|
94
|
+
@origin == connection.origin && connection.options.fallback_protocol == "h2c"
|
95
|
+
end
|
96
|
+
|
97
|
+
def upgrade(request, response)
|
98
|
+
@parser.reset if @parser
|
99
|
+
@parser = H2CParser.new(@write_buffer, @options)
|
100
|
+
set_parser_callbacks(@parser)
|
101
|
+
@parser.upgrade(request, response)
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_parser(*)
|
105
|
+
return super unless @origin.scheme == "http"
|
106
|
+
|
107
|
+
super("http/1.1")
|
69
108
|
end
|
70
109
|
end
|
71
110
|
|
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
module HTTPX
|
4
4
|
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds support for passing `http-form_data` objects (like file objects) as "multipart/form-data";
|
7
|
+
#
|
8
|
+
# HTTPX.post(URL, form: form: { image: HTTP::FormData::File.new("path/to/file")})
|
9
|
+
#
|
5
10
|
module Multipart
|
6
11
|
module FormTranscoder
|
7
12
|
module_function
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
# This plugin implements a session that persists connections over the duration of the process.
|
6
|
+
#
|
7
|
+
# This will improve connection reuse in a long-running process.
|
8
|
+
#
|
9
|
+
# One important caveat to note is, although this session might not close connections,
|
10
|
+
# other sessions from the same process that don't have this plugin turned on might.
|
11
|
+
#
|
12
|
+
# This session will still be able to work with it, as if, when expecting a connection
|
13
|
+
# terminated by a different session, it will just retry on a new one and keep it open.
|
14
|
+
#
|
15
|
+
# This plugin is also not recommendable when connecting to >9000 (like, a lot) different origins.
|
16
|
+
# So when you use this, make sure that you don't fall into this trap.
|
17
|
+
#
|
18
|
+
module Persistent
|
19
|
+
def self.load_dependencies(klass, *)
|
20
|
+
klass.plugin(:retries) # TODO: pass default max_retries -> 1 as soon as this is a parameter
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.extra_options(options)
|
24
|
+
options.merge(persistent: true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
register_plugin :persistent, Persistent
|
28
|
+
end
|
29
|
+
end
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -6,11 +6,19 @@ require "forwardable"
|
|
6
6
|
|
7
7
|
module HTTPX
|
8
8
|
module Plugins
|
9
|
+
#
|
10
|
+
# This plugin adds support for proxies. It ships with support for:
|
11
|
+
#
|
12
|
+
# * HTTP proxies
|
13
|
+
# * HTTPS proxies
|
14
|
+
# * Socks4/4a proxies
|
15
|
+
# * Socks5 proxies
|
16
|
+
#
|
9
17
|
module Proxy
|
10
18
|
Error = Class.new(Error)
|
11
|
-
|
12
|
-
extend Registry
|
19
|
+
PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
|
13
20
|
|
21
|
+
class Parameters
|
14
22
|
attr_reader :uri, :username, :password
|
15
23
|
|
16
24
|
def initialize(uri:, username: nil, password: nil)
|
@@ -26,6 +34,32 @@ module HTTPX
|
|
26
34
|
def token_authentication
|
27
35
|
Base64.strict_encode64("#{user}:#{password}")
|
28
36
|
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
if other.is_a?(Parameters)
|
40
|
+
@uri == other.uri &&
|
41
|
+
@username == other.username &&
|
42
|
+
@password == other.password
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def configure(klass, *)
|
51
|
+
klass.plugin(:"proxy/http")
|
52
|
+
klass.plugin(:"proxy/socks4")
|
53
|
+
klass.plugin(:"proxy/socks5")
|
54
|
+
end
|
55
|
+
|
56
|
+
def extra_options(options)
|
57
|
+
Class.new(options.class) do
|
58
|
+
def_option(:proxy) do |pr|
|
59
|
+
Hash[pr]
|
60
|
+
end
|
61
|
+
end.new(options)
|
62
|
+
end
|
29
63
|
end
|
30
64
|
|
31
65
|
module InstanceMethods
|
@@ -35,118 +69,131 @@ module HTTPX
|
|
35
69
|
|
36
70
|
private
|
37
71
|
|
38
|
-
def
|
72
|
+
def proxy_uris(uri, options)
|
39
73
|
@_proxy_uris ||= begin
|
40
|
-
uris =
|
74
|
+
uris = options.proxy ? Array(options.proxy[:uri]) : []
|
41
75
|
if uris.empty?
|
42
76
|
uri = URI(uri).find_proxy
|
43
77
|
uris << uri if uri
|
44
78
|
end
|
45
79
|
uris
|
46
80
|
end
|
47
|
-
|
81
|
+
options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
|
48
82
|
end
|
49
83
|
|
50
|
-
def
|
84
|
+
def find_connection(request, connections, options)
|
85
|
+
return super unless options.respond_to?(:proxy)
|
86
|
+
|
51
87
|
uri = URI(request.uri)
|
52
|
-
|
53
|
-
raise Error, "Failed to connect to proxy" unless
|
54
|
-
|
88
|
+
next_proxy = proxy_uris(uri, options)
|
89
|
+
raise Error, "Failed to connect to proxy" unless next_proxy
|
90
|
+
|
91
|
+
proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
|
92
|
+
connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
|
93
|
+
unless connections.nil? || connections.include?(connection)
|
94
|
+
connections << connection
|
95
|
+
set_connection_callbacks(connection, options)
|
96
|
+
end
|
97
|
+
connection
|
55
98
|
end
|
56
99
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
set_channel_callbacks(channel, options)
|
61
|
-
channel
|
62
|
-
end
|
100
|
+
def build_connection(uri, options)
|
101
|
+
proxy = options.proxy
|
102
|
+
return super unless proxy
|
63
103
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
log { "proxy: #{uri}" }
|
68
|
-
proxy_type = Parameters.registry(parameters.uri.scheme)
|
69
|
-
channel = proxy_type.new("tcp", uri, parameters, @options.merge(options), &method(:on_response))
|
70
|
-
@connection.__send__(:resolve_channel, channel)
|
71
|
-
channel
|
104
|
+
connection = options.connection_class.new("tcp", uri, options)
|
105
|
+
pool.init_connection(connection, options)
|
106
|
+
connection
|
72
107
|
end
|
73
108
|
|
74
|
-
def fetch_response(request)
|
109
|
+
def fetch_response(request, connections, options)
|
75
110
|
response = super
|
76
111
|
if response.is_a?(ErrorResponse) &&
|
77
112
|
# either it was a timeout error connecting, or it was a proxy error
|
78
|
-
|
79
|
-
|
80
|
-
!@_proxy_uris.empty?
|
113
|
+
PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
|
114
|
+
@_proxy_uris.shift
|
81
115
|
log { "failed connecting to proxy, trying next..." }
|
82
|
-
|
83
|
-
|
116
|
+
request.transition(:idle)
|
117
|
+
connection = find_connection(request, connections, options)
|
118
|
+
connections << connection unless connections.include?(connection)
|
119
|
+
connection.send(request)
|
84
120
|
return
|
85
121
|
end
|
86
122
|
response
|
87
123
|
end
|
88
124
|
end
|
89
125
|
|
90
|
-
module
|
91
|
-
|
126
|
+
module ConnectionMethods
|
127
|
+
using URIExtensions
|
128
|
+
|
129
|
+
def initialize(*)
|
92
130
|
super
|
93
|
-
|
94
|
-
|
95
|
-
|
131
|
+
return unless @options.proxy
|
132
|
+
|
133
|
+
# redefining the connection origin as the proxy's URI,
|
134
|
+
# as this will be used as the tcp peer ip.
|
135
|
+
@origin = URI(@options.proxy.uri.origin)
|
96
136
|
end
|
97
|
-
end
|
98
137
|
|
99
|
-
|
100
|
-
|
101
|
-
klass.plugin(:"proxy/socks4")
|
102
|
-
klass.plugin(:"proxy/socks5")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
register_plugin :proxy, Proxy
|
106
|
-
end
|
138
|
+
def match?(uri, options)
|
139
|
+
return super unless @options.proxy
|
107
140
|
|
108
|
-
|
109
|
-
|
110
|
-
super(type, uri, options, &blk)
|
111
|
-
@parameters = parameters
|
112
|
-
end
|
141
|
+
super && @options.proxy == options.proxy
|
142
|
+
end
|
113
143
|
|
114
|
-
|
115
|
-
|
116
|
-
|
144
|
+
# should not coalesce connections here, as the IP is the IP of the proxy
|
145
|
+
def coalescable?(*)
|
146
|
+
return super unless @options.proxy
|
117
147
|
|
118
|
-
|
119
|
-
|
120
|
-
end
|
148
|
+
false
|
149
|
+
end
|
121
150
|
|
122
|
-
|
123
|
-
|
124
|
-
|
151
|
+
def send(request)
|
152
|
+
return super unless @options.proxy
|
153
|
+
return super unless connecting?
|
125
154
|
|
126
|
-
|
127
|
-
|
128
|
-
when :idle
|
129
|
-
transition(:connecting)
|
130
|
-
when :connected
|
131
|
-
transition(:open)
|
132
|
-
end
|
133
|
-
@io.to_io
|
134
|
-
end
|
155
|
+
@pending << request
|
156
|
+
end
|
135
157
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
158
|
+
def connecting?
|
159
|
+
return super unless @options.proxy
|
160
|
+
|
161
|
+
super || @state == :connecting || @state == :connected
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_io
|
165
|
+
return super unless @options.proxy
|
143
166
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
167
|
+
case @state
|
168
|
+
when :idle
|
169
|
+
transition(:connecting)
|
170
|
+
when :connected
|
171
|
+
transition(:open)
|
172
|
+
end
|
173
|
+
@io.to_io
|
174
|
+
end
|
175
|
+
|
176
|
+
def call
|
177
|
+
super
|
178
|
+
return unless @options.proxy
|
179
|
+
|
180
|
+
case @state
|
181
|
+
when :connecting
|
182
|
+
consume
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def reset
|
187
|
+
return super unless @options.proxy
|
188
|
+
|
189
|
+
@state = :open
|
190
|
+
transition(:closing)
|
191
|
+
transition(:closed)
|
192
|
+
emit(:close)
|
193
|
+
end
|
194
|
+
end
|
149
195
|
end
|
196
|
+
register_plugin :proxy, Proxy
|
150
197
|
end
|
151
198
|
|
152
199
|
class ProxySSL < SSL
|