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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/httpx.rb +8 -2
  3. data/lib/httpx/adapters/faraday.rb +203 -0
  4. data/lib/httpx/altsvc.rb +4 -0
  5. data/lib/httpx/callbacks.rb +1 -4
  6. data/lib/httpx/chainable.rb +4 -3
  7. data/lib/httpx/connection.rb +326 -104
  8. data/lib/httpx/{channel → connection}/http1.rb +29 -15
  9. data/lib/httpx/{channel → connection}/http2.rb +12 -6
  10. data/lib/httpx/errors.rb +2 -0
  11. data/lib/httpx/headers.rb +4 -1
  12. data/lib/httpx/io/ssl.rb +5 -1
  13. data/lib/httpx/io/tcp.rb +13 -7
  14. data/lib/httpx/io/udp.rb +1 -0
  15. data/lib/httpx/io/unix.rb +1 -0
  16. data/lib/httpx/loggable.rb +34 -9
  17. data/lib/httpx/options.rb +57 -31
  18. data/lib/httpx/parser/http1.rb +8 -0
  19. data/lib/httpx/plugins/authentication.rb +4 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +4 -0
  21. data/lib/httpx/plugins/compression.rb +22 -5
  22. data/lib/httpx/plugins/cookies.rb +89 -36
  23. data/lib/httpx/plugins/digest_authentication.rb +45 -26
  24. data/lib/httpx/plugins/follow_redirects.rb +61 -62
  25. data/lib/httpx/plugins/h2c.rb +78 -39
  26. data/lib/httpx/plugins/multipart.rb +5 -0
  27. data/lib/httpx/plugins/persistent.rb +29 -0
  28. data/lib/httpx/plugins/proxy.rb +125 -78
  29. data/lib/httpx/plugins/proxy/http.rb +31 -27
  30. data/lib/httpx/plugins/proxy/socks4.rb +30 -24
  31. data/lib/httpx/plugins/proxy/socks5.rb +49 -39
  32. data/lib/httpx/plugins/proxy/ssh.rb +81 -0
  33. data/lib/httpx/plugins/push_promise.rb +18 -9
  34. data/lib/httpx/plugins/retries.rb +43 -15
  35. data/lib/httpx/pool.rb +159 -0
  36. data/lib/httpx/registry.rb +2 -0
  37. data/lib/httpx/request.rb +10 -0
  38. data/lib/httpx/resolver.rb +2 -1
  39. data/lib/httpx/resolver/https.rb +62 -56
  40. data/lib/httpx/resolver/native.rb +48 -37
  41. data/lib/httpx/resolver/resolver_mixin.rb +16 -11
  42. data/lib/httpx/resolver/system.rb +11 -7
  43. data/lib/httpx/response.rb +24 -10
  44. data/lib/httpx/selector.rb +32 -39
  45. data/lib/httpx/{client.rb → session.rb} +99 -62
  46. data/lib/httpx/timeout.rb +7 -15
  47. data/lib/httpx/transcoder/body.rb +4 -0
  48. data/lib/httpx/transcoder/chunker.rb +4 -0
  49. data/lib/httpx/version.rb +1 -1
  50. metadata +10 -8
  51. data/lib/httpx/channel.rb +0 -367
@@ -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, keep_open: @keep_open, **options)
12
- return super if @_h2c_probed
13
- begin
14
- requests = __build_reqs(*args, **options)
15
-
16
- upgrade_request = requests.first
17
- return super unless valid_h2c_upgrade_request?(upgrade_request)
18
- upgrade_request.headers["upgrade"] = "h2c"
19
- upgrade_request.headers.add("connection", "upgrade")
20
- upgrade_request.headers.add("connection", "http2-settings")
21
- upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(@options.http2_settings)
22
- # TODO: validate!
23
- upgrade_response = __send_reqs(*upgrade_request, **options).first
24
-
25
- if upgrade_response.status == 101
26
- channel = find_channel(upgrade_request)
27
- parser = channel.upgrade_parser("h2")
28
- parser.extend(UpgradeExtensions)
29
- parser.upgrade(upgrade_request, upgrade_response, **options)
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
- module UpgradeExtensions
62
- def upgrade(request, _response, **)
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
@@ -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
- class Parameters
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 proxy_params(uri)
72
+ def proxy_uris(uri, options)
39
73
  @_proxy_uris ||= begin
40
- uris = @options.proxy ? Array(@options.proxy[:uri]) : []
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
- @options.proxy.merge(uri: @_proxy_uris.shift) unless @_proxy_uris.empty?
81
+ options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
48
82
  end
49
83
 
50
- def find_channel(request, **options)
84
+ def find_connection(request, connections, options)
85
+ return super unless options.respond_to?(:proxy)
86
+
51
87
  uri = URI(request.uri)
52
- proxy = proxy_params(uri)
53
- raise Error, "Failed to connect to proxy" unless proxy
54
- @connection.find_channel(proxy) || build_channel(proxy, options)
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 build_channel(proxy, options)
58
- return super if proxy.is_a?(URI::Generic)
59
- channel = build_proxy_channel(proxy, **options)
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
- def build_proxy_channel(proxy, **options)
65
- parameters = Parameters.new(**proxy)
66
- uri = parameters.uri
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
- (((response.error.is_a?(TimeoutError) || response.error.is_a?(IOError)) && request.state == :idle) ||
79
- response.error.is_a?(Error)) &&
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
- channel = find_channel(request)
83
- channel.send(request)
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 OptionsMethods
91
- def self.included(klass)
126
+ module ConnectionMethods
127
+ using URIExtensions
128
+
129
+ def initialize(*)
92
130
  super
93
- klass.def_option(:proxy) do |pr|
94
- Hash[pr]
95
- end
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
- def self.configure(klass, *)
100
- klass.plugin(:"proxy/http")
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
- class ProxyChannel < Channel
109
- def initialize(type, uri, parameters, options, &blk)
110
- super(type, uri, options, &blk)
111
- @parameters = parameters
112
- end
141
+ super && @options.proxy == options.proxy
142
+ end
113
143
 
114
- def match?(*)
115
- true
116
- end
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
- def send(request, **args)
119
- @pending << [request, args]
120
- end
148
+ false
149
+ end
121
150
 
122
- def connecting?
123
- super || @state == :connecting || @state == :connected
124
- end
151
+ def send(request)
152
+ return super unless @options.proxy
153
+ return super unless connecting?
125
154
 
126
- def to_io
127
- case @state
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
- def call
137
- super
138
- case @state
139
- when :connecting
140
- consume
141
- end
142
- end
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
- def reset
145
- @state = :open
146
- transition(:closing)
147
- transition(:closed)
148
- emit(:close)
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