httpx 0.18.6 → 0.19.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -1
  3. data/doc/release_notes/0_18_5.md +2 -2
  4. data/doc/release_notes/0_18_7.md +5 -0
  5. data/doc/release_notes/0_19_0.md +39 -0
  6. data/doc/release_notes/0_19_1.md +5 -0
  7. data/doc/release_notes/0_19_2.md +7 -0
  8. data/lib/httpx/adapters/faraday.rb +7 -3
  9. data/lib/httpx/connection/http1.rb +5 -5
  10. data/lib/httpx/connection/http2.rb +1 -5
  11. data/lib/httpx/connection.rb +22 -10
  12. data/lib/httpx/extensions.rb +16 -0
  13. data/lib/httpx/headers.rb +0 -2
  14. data/lib/httpx/io/tcp.rb +27 -6
  15. data/lib/httpx/options.rb +44 -11
  16. data/lib/httpx/plugins/cookies.rb +5 -7
  17. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  18. data/lib/httpx/plugins/multipart/mime_type_detector.rb +5 -1
  19. data/lib/httpx/plugins/proxy/http.rb +10 -23
  20. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  21. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  22. data/lib/httpx/plugins/proxy.rb +20 -12
  23. data/lib/httpx/plugins/retries.rb +1 -1
  24. data/lib/httpx/pool.rb +40 -20
  25. data/lib/httpx/resolver/https.rb +32 -42
  26. data/lib/httpx/resolver/multi.rb +79 -0
  27. data/lib/httpx/resolver/native.rb +28 -36
  28. data/lib/httpx/resolver/resolver.rb +93 -0
  29. data/lib/httpx/resolver/system.rb +175 -19
  30. data/lib/httpx/resolver.rb +37 -11
  31. data/lib/httpx/response.rb +4 -2
  32. data/lib/httpx/session.rb +1 -15
  33. data/lib/httpx/session_extensions.rb +26 -0
  34. data/lib/httpx/timers.rb +1 -1
  35. data/lib/httpx/transcoder/chunker.rb +0 -1
  36. data/lib/httpx/version.rb +1 -1
  37. data/lib/httpx.rb +3 -0
  38. data/sig/connection/http1.rbs +0 -2
  39. data/sig/connection/http2.rbs +2 -2
  40. data/sig/connection.rbs +1 -0
  41. data/sig/errors.rbs +8 -0
  42. data/sig/headers.rbs +0 -2
  43. data/sig/httpx.rbs +4 -0
  44. data/sig/options.rbs +10 -7
  45. data/sig/parser/http1.rbs +14 -5
  46. data/sig/pool.rbs +17 -9
  47. data/sig/registry.rbs +3 -0
  48. data/sig/request.rbs +11 -0
  49. data/sig/resolver/https.rbs +15 -27
  50. data/sig/resolver/multi.rbs +7 -0
  51. data/sig/resolver/native.rbs +3 -12
  52. data/sig/resolver/resolver.rbs +36 -0
  53. data/sig/resolver/system.rbs +3 -9
  54. data/sig/resolver.rbs +12 -10
  55. data/sig/response.rbs +15 -5
  56. data/sig/selector.rbs +3 -3
  57. data/sig/timers.rbs +5 -2
  58. data/sig/transcoder/chunker.rbs +16 -5
  59. data/sig/transcoder/json.rbs +5 -0
  60. data/sig/transcoder.rbs +3 -1
  61. metadata +15 -4
  62. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  63. data/sig/resolver/resolver_mixin.rbs +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6784fbd393b5caed888e6c4d1a421cde9c7aca49f35baaafbf13f10a68e9d02
4
- data.tar.gz: ddd5bb8793fa72b19e462b411f5eaaaf553f30d589a68dfdde64224c49d902f6
3
+ metadata.gz: b9e79c1b196931855e7af542bd611050a95648d2888d539cb20de5026c4a3ccf
4
+ data.tar.gz: 4cc919ede2c58d6bcf86433ddf4ceba9fdca5416186d5984fa2f29a92707668d
5
5
  SHA512:
6
- metadata.gz: 949f18741f10eaa57463b8764569e69cdfb1ca6f4eefa30bbdcf142cbbd8b61b360d9bd78898a816a4ebd9e15d4ffd1c0d8282033a459826919ce8132d5b105f
7
- data.tar.gz: e4430eea967c9f5628d51e6150bc4f63e704eaed2bd61fda191f3539a3f102e00932cc8f63ddb735c33bbf21e2694bb23fed5eaea588159e71fa6f484016c150
6
+ metadata.gz: 5a35e9f740d6df65190a8e28cc2d2d8adfe9c208842290ceb0c8f6c9dd273e19066c7cc851d917f6daac663c205401e7df26916a0f41c01653e53488d404f669
7
+ data.tar.gz: 67203d583847abee46ac940ff82c11624298907e191ff0c6c5408b1cddf0c909ac8820de24f11ef1cc9ac7030e5d11e807668e9f991967fc4bb4654f92ddd3c8
data/README.md CHANGED
@@ -140,11 +140,15 @@ In order to use HTTP/2 under JRuby, [check this link](https://gitlab.com/honeyry
140
140
  * Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/honeyryderchuck/httpx/issues/36)).
141
141
  * Using `total_timeout` along with the `:persistent` plugin [does not work as you might expect](https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts#total_timeout).
142
142
 
143
+ ## Versioning Policy
144
+
145
+ Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.
146
+
143
147
  ## Contributing
144
148
 
145
149
  * Discuss your contribution in an issue
146
150
  * Fork it
147
151
  * Make your changes, add some tests
148
- * Ensure all tests pass (`bundle exec rake test`)
152
+ * Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
149
153
  * Open a Merge Request (that's Pull Request in Github-ish)
150
154
  * Wait for feedback
@@ -3,8 +3,8 @@
3
3
  ## Improvements
4
4
 
5
5
  * ruby 3.1 is now officially supported.
6
- * when user sets a `Host` header for an HTTP/2 request, this will be used in the `:authority` HTTP/2 pseudo-header, instead of silently ignored (mimicking what "curl" does).
6
+ * when a user sets a `Host` header for an HTTP/2 request, this will be used in the `:authority` HTTP/2 pseudo-header, instead of silently ignored (mimicking what "curl" does).
7
7
 
8
8
  ## Bugfixes
9
9
 
10
- * fixed "throw outside of catch block" error happening when pipelining requests on an HTTP/1 connnection and resulting in a timeout.
10
+ * fixed "throw outside of catch block" error happening when pipelining requests on an HTTP/1 connection and resulting in a timeout.
@@ -0,0 +1,5 @@
1
+ # 0.18.6
2
+
3
+ ## Bugfixes
4
+
5
+ * multipart plugin: fixed `filemagic` integration by rewinding the file after mime-type detection.
@@ -0,0 +1,39 @@
1
+ # 0.19.0
2
+
3
+ ## Features
4
+
5
+ ### Happy Eyeballs v2
6
+
7
+ When the system supports dual-stack networking, `httpx` implements the Happy Eyeballs v2 algorithm (RFC 8305) to resolve hostnames to both IPv6 and IPv4 addresses while privileging IPv6 connectivity. This is implemented by `httpx` both for the `:native` as well as the `:https` (DoH) resolver (which do not perform address sorting, thereby being "DNS-based load-balancing" friendly), and "outsourced" to `getaddrinfo` when using the `:system` resolver.
8
+
9
+ IPv6 connectivity will also be privileged for `/etc/hosts` local DNS (i.e. `localhost` connections will connec to `::1`).
10
+
11
+ A new option, `:ip_families`, will also be available (`[Socket::AF_INET6, Socket::AF_INET]` in dual-stack systems). If you'd like to i.e. force IPv4 connectivity, you can do use it (`client = HTTPX.with(ip_families: [Socket::AF_INET])`).
12
+
13
+ ## Improvements
14
+
15
+ ### DNS: :system resolver uses getaddrinfo (instead of the resolver lib)
16
+
17
+ The `:system` resolver switched to using the `getaddinfo` system function to perform DNS requests. Not only is this call **not** blocking the session event loop anymore (unlike pre-0.19.0 `:system` resolver), it adds a lot of functionality that the stdlib `resolv` library just doesn't support at the moment (such as SRV records).
18
+
19
+ ### HTTP/2 proxy support
20
+
21
+ The `:proxy` plugin handles "prior-knowledge" HTTP/2 proxies.
22
+
23
+ ```ruby
24
+ HTTPX.plugin(:proxy, fallback_protocol: "h2").with_proxy(uri: "http://http2-proxy:3128").get(...
25
+ ```
26
+
27
+ Connection coalescing has also been enabled for proxied connections (also `CONNECT`-tunneled connections).
28
+
29
+ ### curl-to-httpx
30
+
31
+ widget in [project website](https://honeyryderchuck.gitlab.io/httpx/) to turn curl commands into the equivalent `httpx` code.
32
+
33
+ ## Bugfixes
34
+
35
+ * faraday adapter now supports passing session options.
36
+ * proxy: several fixes which enabled env-var (`HTTP(S)_PROXY`) defined proxy support.
37
+ * proxy: fixed graceful recovery from proxy tcp connect errors.
38
+ * several fixes around CNAMEs timeouts with the native resolver.
39
+ * https resolver is now closed when wrapping session closes (it was left open).
@@ -0,0 +1,5 @@
1
+ # 0.19.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixing a DNS dual-stack case where one the resolvers may have finished way before the previous one and will therefore return no timeout.
@@ -0,0 +1,7 @@
1
+ # 0.19.2
2
+
3
+ ## Bugfixes
4
+
5
+ * skip resolution delay path for early resolve cases
6
+
7
+ when the early resolve path (using IP, /etc/hosts IP, IP from cache) is followed, emit_addresses is called, and in a particular case (dual-stack network but using an IPv4 address), the happy eyeballs resolution delay path was activated when it shouldn't (it's meant to be used only for DNS network requests), and resulted in @pool being called before it was ever set. This simple check ensures that it doesn't happen before it must.
@@ -81,6 +81,9 @@ module Faraday
81
81
 
82
82
  def response=(response)
83
83
  super
84
+
85
+ return if response.is_a?(::HTTPX::ErrorResponse)
86
+
84
87
  response.body.on_data = @response_on_data
85
88
  end
86
89
  end
@@ -136,7 +139,7 @@ module Faraday
136
139
 
137
140
  def on_response(&blk)
138
141
  if blk
139
- @on_response = lambda do |response|
142
+ @on_response = ->(response) do
140
143
  blk.call(response)
141
144
  end
142
145
  self
@@ -200,9 +203,9 @@ module Faraday
200
203
  end
201
204
  end
202
205
 
203
- def initialize(app)
206
+ def initialize(app, options = {})
204
207
  super(app)
205
- @session = Session.new
208
+ @session = Session.new(options)
206
209
  end
207
210
 
208
211
  def call(env)
@@ -210,6 +213,7 @@ module Faraday
210
213
  if parallel?(env)
211
214
  handler = env[:parallel_manager].enqueue(env)
212
215
  handler.on_response do |response|
216
+ response.raise_for_status
213
217
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
214
218
  response_headers.merge!(response.headers)
215
219
  end
@@ -297,10 +297,6 @@ module HTTPX
297
297
  extra_headers
298
298
  end
299
299
 
300
- def headline_uri(request)
301
- request.path
302
- end
303
-
304
300
  def handle(request)
305
301
  catch(:buffer_full) do
306
302
  request.transition(:headers)
@@ -314,8 +310,12 @@ module HTTPX
314
310
  end
315
311
  end
316
312
 
313
+ def join_headline(request)
314
+ "#{request.verb.to_s.upcase} #{request.path} HTTP/#{@version.join(".")}"
315
+ end
316
+
317
317
  def join_headers(request)
318
- headline = "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}"
318
+ headline = join_headline(request)
319
319
  @buffer << headline << CRLF
320
320
  log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
321
321
  extra_headers = set_protocol_headers(request)
@@ -158,10 +158,6 @@ module HTTPX
158
158
  end
159
159
  end
160
160
 
161
- def headline_uri(request)
162
- request.path
163
- end
164
-
165
161
  def handle(request, stream)
166
162
  catch(:buffer_full) do
167
163
  request.transition(:headers)
@@ -213,7 +209,7 @@ module HTTPX
213
209
  {
214
210
  ":scheme" => request.scheme,
215
211
  ":method" => request.verb.to_s.upcase,
216
- ":path" => headline_uri(request),
212
+ ":path" => request.path,
217
213
  ":authority" => request.authority,
218
214
  }
219
215
  end
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  def_delegator :@write_buffer, :empty?
46
46
 
47
- attr_reader :origin, :state, :pending, :options
47
+ attr_reader :io, :origin, :origins, :state, :pending, :options
48
48
 
49
49
  attr_writer :timers
50
50
 
@@ -78,7 +78,11 @@ module HTTPX
78
78
  # this is a semi-private method, to be used by the resolver
79
79
  # to initiate the io object.
80
80
  def addresses=(addrs)
81
- @io ||= IO.registry(@type).new(@origin, addrs, @options) # rubocop:disable Naming/MemoizedInstanceVariableName
81
+ if @io
82
+ @io.add_addresses(addrs)
83
+ else
84
+ @io = IO.registry(@type).new(@origin, addrs, @options)
85
+ end
82
86
  end
83
87
 
84
88
  def addresses
@@ -489,6 +493,18 @@ module HTTPX
489
493
  end
490
494
 
491
495
  def transition(nextstate)
496
+ handle_transition(nextstate)
497
+ rescue Errno::ECONNREFUSED,
498
+ Errno::EADDRNOTAVAIL,
499
+ Errno::EHOSTUNREACH,
500
+ TLSError => e
501
+ # connect errors, exit gracefully
502
+ handle_error(e)
503
+ @state = :closed
504
+ emit(:close)
505
+ end
506
+
507
+ def handle_transition(nextstate)
492
508
  case nextstate
493
509
  when :idle
494
510
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
@@ -525,14 +541,6 @@ module HTTPX
525
541
  emit(:activate)
526
542
  end
527
543
  @state = nextstate
528
- rescue Errno::ECONNREFUSED,
529
- Errno::EADDRNOTAVAIL,
530
- Errno::EHOSTUNREACH,
531
- TLSError => e
532
- # connect errors, exit gracefully
533
- handle_error(e)
534
- @state = :closed
535
- emit(:close)
536
544
  end
537
545
 
538
546
  def purge_after_closed
@@ -550,6 +558,10 @@ module HTTPX
550
558
  ex.set_backtrace(error.backtrace)
551
559
  error = ex
552
560
  else
561
+ # inactive connections do not contribute to the select loop, therefore
562
+ # they should fail due to such errors.
563
+ return if @state == :inactive
564
+
553
565
  if @timeout
554
566
  @timeout -= error.timeout
555
567
  return unless @timeout <= 0
@@ -54,6 +54,22 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module StringExtensions
58
+ refine String do
59
+ def delete_suffix!(suffix)
60
+ suffix = Backports.coerce_to_str(suffix)
61
+ chomp! if frozen?
62
+ len = suffix.length
63
+ if len > 0 && index(suffix, -len)
64
+ self[-len..-1] = ''
65
+ self
66
+ else
67
+ nil
68
+ end
69
+ end unless String.method_defined?(:delete_suffix!)
70
+ end
71
+ end
72
+
57
73
  module HashExtensions
58
74
  refine Hash do
59
75
  def compact
data/lib/httpx/headers.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module HTTPX
4
4
  class Headers
5
- EMPTY = [].freeze
6
-
7
5
  class << self
8
6
  def new(headers = nil)
9
7
  return headers if headers.is_a?(self)
data/lib/httpx/io/tcp.rb CHANGED
@@ -15,6 +15,7 @@ module HTTPX
15
15
 
16
16
  def initialize(origin, addresses, options)
17
17
  @state = :idle
18
+ @addresses = []
18
19
  @hostname = origin.host
19
20
  @options = Options.new(options)
20
21
  @fallback_protocol = @options.fallback_protocol
@@ -30,15 +31,29 @@ module HTTPX
30
31
  raise Error, "Given IO objects do not match the request authority" unless @io
31
32
 
32
33
  _, _, _, @ip = @io.addr
33
- @addresses ||= [@ip]
34
- @ip_index = @addresses.size - 1
34
+ @addresses << @ip
35
35
  @keep_open = true
36
36
  @state = :connected
37
37
  else
38
- @addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
38
+ add_addresses(addresses)
39
39
  end
40
40
  @ip_index = @addresses.size - 1
41
- @io ||= build_socket
41
+ # @io ||= build_socket
42
+ end
43
+
44
+ def add_addresses(addrs)
45
+ return if addrs.empty?
46
+
47
+ addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
48
+
49
+ ip_index = @ip_index || (@addresses.size - 1)
50
+ if addrs.first.ipv6?
51
+ # should be the next in line
52
+ @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
53
+ else
54
+ @addresses.unshift(*addrs)
55
+ @ip_index += addrs.size if @ip_index
56
+ end
42
57
  end
43
58
 
44
59
  def to_io
@@ -52,20 +67,26 @@ module HTTPX
52
67
  def connect
53
68
  return unless closed?
54
69
 
55
- if @io.closed?
70
+ if !@io || @io.closed?
56
71
  transition(:idle)
57
72
  @io = build_socket
58
73
  end
59
74
  try_connect
60
- rescue Errno::EHOSTUNREACH => e
75
+ rescue Errno::ECONNREFUSED,
76
+ Errno::EADDRNOTAVAIL,
77
+ Errno::EHOSTUNREACH => e
61
78
  raise e if @ip_index <= 0
62
79
 
80
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
63
81
  @ip_index -= 1
82
+ @io = build_socket
64
83
  retry
65
84
  rescue Errno::ETIMEDOUT => e
66
85
  raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
67
86
 
87
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
68
88
  @ip_index -= 1
89
+ @io = build_socket
69
90
  retry
70
91
  end
71
92
 
data/lib/httpx/options.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+
3
5
  module HTTPX
4
6
  class Options
5
7
  WINDOW_SIZE = 1 << 14 # 16K
@@ -9,6 +11,18 @@ module HTTPX
9
11
  KEEP_ALIVE_TIMEOUT = 20
10
12
  SETTINGS_TIMEOUT = 10
11
13
 
14
+ # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
15
+ ip_address_families = begin
16
+ list = Socket.ip_address_list
17
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
18
+ [Socket::AF_INET6, Socket::AF_INET]
19
+ else
20
+ [Socket::AF_INET]
21
+ end
22
+ rescue NotImplementedError
23
+ [Socket::AF_INET]
24
+ end
25
+
12
26
  DEFAULT_OPTIONS = {
13
27
  :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
14
28
  :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
@@ -37,6 +51,7 @@ module HTTPX
37
51
  :persistent => false,
38
52
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
39
53
  :resolver_options => { cache: true },
54
+ :ip_families => ip_address_families,
40
55
  }.freeze
41
56
 
42
57
  begin
@@ -110,20 +125,18 @@ module HTTPX
110
125
  end
111
126
 
112
127
  def initialize(options = {})
113
- defaults = DEFAULT_OPTIONS.merge(options)
114
- defaults.each do |k, v|
115
- next if v.nil?
116
-
117
- begin
118
- value = __send__(:"option_#{k}", v)
119
- instance_variable_set(:"@#{k}", value)
120
- rescue NoMethodError
121
- raise Error, "unknown option: #{k}"
122
- end
123
- end
128
+ __initialize__(options)
124
129
  freeze
125
130
  end
126
131
 
132
+ def freeze
133
+ super
134
+ @origin.freeze
135
+ @timeout.freeze
136
+ @headers.freeze
137
+ @addresses.freeze
138
+ end
139
+
127
140
  def option_origin(value)
128
141
  URI(value)
129
142
  end
@@ -174,6 +187,10 @@ module HTTPX
174
187
  Array(value)
175
188
  end
176
189
 
190
+ def option_ip_families(value)
191
+ Array(value)
192
+ end
193
+
177
194
  %i[
178
195
  params form json body ssl http2_settings
179
196
  request_class response_class headers_class request_body_class
@@ -249,5 +266,21 @@ module HTTPX
249
266
  end
250
267
  end
251
268
  end
269
+
270
+ private
271
+
272
+ def __initialize__(options = {})
273
+ defaults = DEFAULT_OPTIONS.merge(options)
274
+ defaults.each do |k, v|
275
+ next if v.nil?
276
+
277
+ begin
278
+ value = __send__(:"option_#{k}", v)
279
+ instance_variable_set(:"@#{k}", value)
280
+ rescue NoMethodError
281
+ raise Error, "unknown option: #{k}"
282
+ end
283
+ end
284
+ end
252
285
  end
253
286
  end
@@ -18,12 +18,6 @@ module HTTPX
18
18
  require "httpx/plugins/cookies/set_cookie_parser"
19
19
  end
20
20
 
21
- module OptionsMethods
22
- def option_cookies(value)
23
- value.is_a?(Jar) ? value : Jar.new(value)
24
- end
25
- end
26
-
27
21
  module InstanceMethods
28
22
  extend Forwardable
29
23
 
@@ -77,7 +71,7 @@ module HTTPX
77
71
  end
78
72
 
79
73
  module OptionsMethods
80
- def initialize(*)
74
+ def __initialize__(*)
81
75
  super
82
76
 
83
77
  return unless @headers.key?("cookie")
@@ -89,6 +83,10 @@ module HTTPX
89
83
  end
90
84
  end
91
85
  end
86
+
87
+ def option_cookies(value)
88
+ value.is_a?(Jar) ? value : Jar.new(value)
89
+ end
92
90
  end
93
91
  end
94
92
  register_plugin :cookies, Cookies
@@ -81,7 +81,7 @@ module HTTPX
81
81
  super
82
82
  end
83
83
 
84
- def transition(nextstate)
84
+ def handle_transition(nextstate)
85
85
  state = @state
86
86
  super
87
87
  meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
@@ -14,9 +14,13 @@ module HTTPX
14
14
  def call(file, _)
15
15
  return nil if file.eof? # FileMagic returns "application/x-empty" for empty files
16
16
 
17
- FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
17
+ mime = FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
18
18
  filemagic.buffer(file.read(MAGIC_NUMBER))
19
19
  end
20
+
21
+ file.rewind
22
+
23
+ mime
20
24
  end
21
25
  elsif defined?(Marcel)
22
26
  def call(file, filename)
@@ -13,7 +13,7 @@ module HTTPX
13
13
 
14
14
  private
15
15
 
16
- def transition(nextstate)
16
+ def handle_transition(nextstate)
17
17
  return super unless @options.proxy && @options.proxy.uri.scheme == "http"
18
18
 
19
19
  case nextstate
@@ -23,7 +23,8 @@ module HTTPX
23
23
  @io.connect
24
24
  return unless @io.connected?
25
25
 
26
- @parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
26
+ @parser = registry(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
27
+ @parser.extend(ProxyParser)
27
28
  @parser.once(:response, &method(:__http_on_connect))
28
29
  @parser.on(:close) { transition(:closing) }
29
30
  __http_proxy_connect
@@ -36,7 +37,7 @@ module HTTPX
36
37
  @parser.close
37
38
  @parser = nil
38
39
  when :idle
39
- @parser = ProxyParser.new(@write_buffer, @options)
40
+ @parser.callbacks.clear
40
41
  set_parser_callbacks(@parser)
41
42
  end
42
43
  end
@@ -54,7 +55,7 @@ module HTTPX
54
55
  @inflight += 1
55
56
  parser.send(connect_request)
56
57
  else
57
- transition(:connected)
58
+ handle_transition(:connected)
58
59
  end
59
60
  end
60
61
 
@@ -76,9 +77,11 @@ module HTTPX
76
77
  end
77
78
  end
78
79
 
79
- class ProxyParser < Connection::HTTP1
80
- def headline_uri(request)
81
- request.uri.to_s
80
+ module ProxyParser
81
+ def join_headline(request)
82
+ return super if request.verb == :connect
83
+
84
+ "#{request.verb.to_s.upcase} #{request.uri} HTTP/#{@version.join(".")}"
82
85
  end
83
86
 
84
87
  def set_protocol_headers(request)
@@ -91,22 +94,6 @@ module HTTPX
91
94
  end
92
95
  end
93
96
 
94
- class ConnectProxyParser < ProxyParser
95
- attr_reader :pending
96
-
97
- def headline_uri(request)
98
- return super unless request.verb == :connect
99
-
100
- tunnel = request.path
101
- log { "establishing HTTP proxy tunnel to #{tunnel}" }
102
- tunnel
103
- end
104
-
105
- def empty?
106
- @requests.reject { |r| r.verb == :connect }.empty? || @requests.all? { |request| !request.response.nil? }
107
- end
108
- end
109
-
110
97
  class ConnectRequest < Request
111
98
  def initialize(uri, _options)
112
99
  super(:connect, uri, {})
@@ -27,7 +27,7 @@ module HTTPX
27
27
 
28
28
  private
29
29
 
30
- def transition(nextstate)
30
+ def handle_transition(nextstate)
31
31
  return super unless @options.proxy && PROTOCOLS.include?(@options.proxy.uri.scheme)
32
32
 
33
33
  case nextstate
@@ -46,7 +46,7 @@ module HTTPX
46
46
 
47
47
  private
48
48
 
49
- def transition(nextstate)
49
+ def handle_transition(nextstate)
50
50
  return super unless @options.proxy && @options.proxy.uri.scheme == "socks5"
51
51
 
52
52
  case nextstate