httpx 0.18.3 → 0.18.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81b7e14071e0e1978c864c63b0c8146044f255a9b2f7d9d3e917b564fb301ffb
4
- data.tar.gz: 7492d34e586d86e373f62f151ceb6962719fcec156597ff34dae4c10b8347072
3
+ metadata.gz: ab9b9577b0e266b140e21030307b3f2436af346a13f1e0931af05020ba42f7e6
4
+ data.tar.gz: c5d043448ef41b0532c08d1622540d80c68511ef59cffcc810a6f8bd92f348d5
5
5
  SHA512:
6
- metadata.gz: 5c5930cd68b2ba0f42ea60e6dc5a76ff24b2643f16c300b778e8af62f0f04d8c01baadbf0163e2b925275bbfdf44d8dd73bbf78e06dd2c86b2e54d0eb99cbbdc
7
- data.tar.gz: 20a70c6ffc8895c7b54f84f9832bc8831942048ba37dc83907e0b4fef83484d1f6797a0792b075539cb985a8df65798085f3089e6746d1246e9d27ef0cb63d96
6
+ metadata.gz: 8b4ec58f2ab031d23f28a86bc47b40d0f0d79eea9aea64590b0cc3d07269738513de1364608e0c0282a6956d816b78b900520dbad4daab1f73522b8b7f73d138
7
+ data.tar.gz: f2d2c297b530bfb3567ebcd1678bad1d729187dd75c9107839c925def1a708c4648fe80df614a01af02bce309594e256e274e042f3c6b8347182aebff1df9935
@@ -4,4 +4,4 @@
4
4
 
5
5
  * request bodies eager-loaded from enumerables yield duped partial chunks.
6
6
 
7
- An error was observed while looking at webmock integration, where requests formed via the multipart plugin where returning an empty string as body. The issue was caused by an optimization on multipart encoder, which reuses the same buffer when reading chunks. Unfortunately, these cannot be yielded the same way via IO.copy_stream, as the same (cleared) buffer will be used to generate the eager-loaded body chunks.
7
+ An error was observed while looking at webmock integration, where requests formed via the multipart plugin were returning an empty string as body. The issue was caused by an optimization on multipart encoder, which reuses the same buffer when reading chunks. Unfortunately, these cannot be yielded the same way via IO.copy_stream, as the same (cleared) buffer will be used to generate the eager-loaded body chunks.
@@ -0,0 +1,14 @@
1
+ # 0.18.4
2
+
3
+ ## Improvements
4
+
5
+ * faraday adapter: added support for `#on_data` callback in order to support [faraday streaming](https://lostisland.github.io/faraday/usage/streaming).
6
+
7
+ * multipart plugin: removed support for file mime type detection using `mime-types`. The reasoning behind it was that `mime-types` uses the filename, which is a very inaccurate detection strategy (ex: an mp4 video will be identified as `application/mp4`, instead of the correct `video/mp4`).
8
+ * multipart plugin: supported for file mime type detection using `marcel` and `filemagic` was added. Both use the magic header bytes, which is a more accurate strategy for file type detection.
9
+
10
+ ## Bugfixes
11
+
12
+ * webmock adapter has been reimplemented to work with `httpx` plugins (such as the `:retries` plugin). Some other fixes were applied to make it work better under `vcr` (a common `webmock` extension).
13
+
14
+ * fixed the URI-related bug which was making requests stall under ruby 3.1 (still not officially testing against it).
@@ -0,0 +1,10 @@
1
+ # 0.18.5
2
+
3
+ ## Improvements
4
+
5
+ * ruby 3.1 is now officially supported.
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
+
8
+ ## Bugfixes
9
+
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 missing constant in `filemagic` integration.
@@ -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.
@@ -21,6 +21,17 @@ module Faraday
21
21
  end
22
22
  # :nocov:
23
23
 
24
+ unless Faraday::RequestOptions.method_defined?(:stream_response?)
25
+ module RequestOptionsExtensions
26
+ refine Faraday::RequestOptions do
27
+ def stream_response?
28
+ false
29
+ end
30
+ end
31
+ end
32
+ using RequestOptionsExtensions
33
+ end
34
+
24
35
  module RequestMixin
25
36
  using ::HTTPX::HashExtensions
26
37
 
@@ -64,6 +75,27 @@ module Faraday
64
75
 
65
76
  include RequestMixin
66
77
 
78
+ module OnDataPlugin
79
+ module RequestMethods
80
+ attr_writer :response_on_data
81
+
82
+ def response=(response)
83
+ super
84
+ response.body.on_data = @response_on_data
85
+ end
86
+ end
87
+
88
+ module ResponseBodyMethods
89
+ attr_writer :on_data
90
+
91
+ def write(chunk)
92
+ return super unless @on_data
93
+
94
+ @on_data.call(chunk, chunk.bytesize)
95
+ end
96
+ end
97
+ end
98
+
67
99
  class Session < ::HTTPX::Session
68
100
  plugin(:compression)
69
101
  plugin(:persistent)
@@ -137,15 +169,21 @@ module Faraday
137
169
  end
138
170
 
139
171
  def run
140
- requests = @handlers.map { |handler| build_request(handler.env) }
141
172
  env = @handlers.last.env
142
173
 
143
- proxy_options = { uri: env.request.proxy }
144
-
145
174
  session = @session.with(options_from_env(env))
146
- session = session.plugin(:proxy).with(proxy: proxy_options) if env.request.proxy
175
+ session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
176
+ session = session.plugin(OnDataPlugin) if env.request.stream_response?
177
+
178
+ requests = @handlers.map { |handler| session.build_request(*build_request(handler.env)) }
179
+
180
+ if env.request.stream_response?
181
+ requests.each do |request|
182
+ request.response_on_data = env.request.on_data
183
+ end
184
+ end
147
185
 
148
- responses = session.request(requests)
186
+ responses = session.request(*requests)
149
187
  Array(responses).each_with_index do |response, index|
150
188
  handler = @handlers[index]
151
189
  handler.on_response.call(response)
@@ -179,11 +217,15 @@ module Faraday
179
217
  return handler
180
218
  end
181
219
 
182
- meth, uri, request_options = build_request(env)
183
-
184
220
  session = @session.with(options_from_env(env))
185
- session = session.plugin(:proxy).with(proxy: proxy_options) if env.request.proxy
186
- response = session.__send__(meth, uri, **request_options)
221
+ session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
222
+ session = session.plugin(OnDataPlugin) if env.request.stream_response?
223
+
224
+ request = session.build_request(*build_request(env))
225
+
226
+ request.response_on_data = env.request.on_data if env.request.stream_response?
227
+
228
+ response = session.request(request)
187
229
  response.raise_for_status unless response.is_a?(::HTTPX::Response)
188
230
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
189
231
  response_headers.merge!(response.headers)
@@ -16,56 +16,8 @@ module WebMock
16
16
  # Requests are "hijacked" at the session, before they're distributed to a connection.
17
17
  #
18
18
  module Plugin
19
- module InstanceMethods
20
- private
21
-
22
- def send_requests(*requests)
23
- request_signatures = requests.map do |request|
24
- request_signature = _build_webmock_request_signature(request)
25
- WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
26
- request_signature
27
- end
28
-
29
- responses = request_signatures.map do |request_signature|
30
- WebMock::StubRegistry.instance.response_for_request(request_signature)
31
- end
32
-
33
- real_requests = {}
34
-
35
- requests.each_with_index.each_with_object([request_signatures, responses]) do |(request, idx), (sig_reqs, mock_responses)|
36
- if (webmock_response = mock_responses[idx])
37
- mock_responses[idx] = _build_from_webmock_response(request, webmock_response)
38
- WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, sig_reqs[idx], webmock_response)
39
- log { "mocking #{request.uri} with #{mock_responses[idx].inspect}" }
40
- elsif WebMock.net_connect_allowed?(sig_reqs[idx].uri)
41
- log { "performing #{request.uri}" }
42
- real_requests[request] = idx
43
- else
44
- raise WebMock::NetConnectNotAllowedError, sig_reqs[idx]
45
- end
46
- end
47
-
48
- unless real_requests.empty?
49
- reqs = real_requests.keys
50
- reqs.zip(super(*reqs)).each do |req, res|
51
- idx = real_requests[req]
52
-
53
- if WebMock::CallbackRegistry.any_callbacks?
54
- webmock_response = _build_webmock_response(req, res)
55
- WebMock::CallbackRegistry.invoke_callbacks(
56
- { lib: :httpx, real_request: true }, request_signatures[idx],
57
- webmock_response
58
- )
59
- end
60
-
61
- responses[idx] = res
62
- end
63
- end
64
-
65
- responses
66
- end
67
-
68
- def _build_webmock_request_signature(request)
19
+ class << self
20
+ def build_webmock_request_signature(request)
69
21
  uri = WebMock::Util::URI.heuristic_parse(request.uri)
70
22
  uri.path = uri.normalized_path.gsub("[^:]//", "/")
71
23
 
@@ -77,7 +29,7 @@ module WebMock
77
29
  )
78
30
  end
79
31
 
80
- def _build_webmock_response(_request, response)
32
+ def build_webmock_response(_request, response)
81
33
  webmock_response = WebMock::Response.new
82
34
  webmock_response.status = [response.status, HTTP_REASONS[response.status]]
83
35
  webmock_response.body = response.body.to_s
@@ -85,10 +37,10 @@ module WebMock
85
37
  webmock_response
86
38
  end
87
39
 
88
- def _build_from_webmock_response(request, webmock_response)
89
- return _build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
40
+ def build_from_webmock_response(request, webmock_response)
41
+ return build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
90
42
 
91
- return _build_error_response(request, webmock_response.exception) if webmock_response.exception
43
+ return build_error_response(request, webmock_response.exception) if webmock_response.exception
92
44
 
93
45
  response = request.options.response_class.new(request,
94
46
  webmock_response.status[0],
@@ -98,10 +50,70 @@ module WebMock
98
50
  response
99
51
  end
100
52
 
101
- def _build_error_response(request, exception)
53
+ def build_error_response(request, exception)
102
54
  HTTPX::ErrorResponse.new(request, exception, request.options)
103
55
  end
104
56
  end
57
+
58
+ module InstanceMethods
59
+ def build_connection(*)
60
+ connection = super
61
+ connection.once(:unmock_connection) do
62
+ pool.__send__(:resolve_connection, connection)
63
+ pool.__send__(:unregister_connection, connection) unless connection.addresses
64
+ end
65
+ connection
66
+ end
67
+ end
68
+
69
+ module ConnectionMethods
70
+ def initialize(*)
71
+ super
72
+ @mocked = true
73
+ end
74
+
75
+ def open?
76
+ return true if @mocked
77
+
78
+ super
79
+ end
80
+
81
+ def interests
82
+ return if @mocked
83
+
84
+ super
85
+ end
86
+
87
+ def send(request)
88
+ request_signature = Plugin.build_webmock_request_signature(request)
89
+ WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
90
+
91
+ if (mock_response = WebMock::StubRegistry.instance.response_for_request(request_signature))
92
+ response = Plugin.build_from_webmock_response(request, mock_response)
93
+ WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
94
+ log { "mocking #{request.uri} with #{mock_response.inspect}" }
95
+ request.response = response
96
+ request.emit(:response, response)
97
+ elsif WebMock.net_connect_allowed?(request_signature.uri)
98
+ if WebMock::CallbackRegistry.any_callbacks?
99
+ request.on(:response) do |resp|
100
+ unless resp.is_a?(HTTPX::ErrorResponse)
101
+ webmock_response = Plugin.build_webmock_response(request, resp)
102
+ WebMock::CallbackRegistry.invoke_callbacks(
103
+ { lib: :httpx, real_request: true }, request_signature,
104
+ webmock_response
105
+ )
106
+ end
107
+ end
108
+ end
109
+ @mocked = false
110
+ emit(:unmock_connection, self)
111
+ super
112
+ else
113
+ raise WebMock::NetConnectNotAllowedError, request_signature
114
+ end
115
+ end
116
+ end
105
117
  end
106
118
 
107
119
  class HttpxAdapter < HttpLibAdapter
@@ -109,12 +121,12 @@ module WebMock
109
121
 
110
122
  class << self
111
123
  def enable!
112
- @original_session = ::HTTPX::Session
124
+ @original_session = HTTPX::Session
113
125
 
114
- webmock_session = ::HTTPX.plugin(Plugin)
126
+ webmock_session = HTTPX.plugin(Plugin)
115
127
 
116
- ::HTTPX.send(:remove_const, :Session)
117
- ::HTTPX.send(:const_set, :Session, webmock_session.class)
128
+ HTTPX.send(:remove_const, :Session)
129
+ HTTPX.send(:const_set, :Session, webmock_session.class)
118
130
  end
119
131
 
120
132
  def disable!
data/lib/httpx/altsvc.rb CHANGED
@@ -70,7 +70,7 @@ module HTTPX
70
70
 
71
71
  scanner = StringScanner.new(altsvc)
72
72
  until scanner.eos?
73
- alt_origin = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
73
+ alt_service = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
74
74
 
75
75
  alt_params = []
76
76
  loop do
@@ -80,29 +80,45 @@ module HTTPX
80
80
  break if scanner.eos? || scanner.scan(/ *, */)
81
81
  end
82
82
  alt_params = Hash[alt_params.map { |field| field.split("=") }]
83
- yield(parse_altsvc_origin(alt_origin), alt_params)
83
+
84
+ alt_proto, alt_authority = alt_service.split("=")
85
+ alt_origin = parse_altsvc_origin(alt_proto, alt_authority)
86
+ return unless alt_origin
87
+
88
+ yield(alt_origin, alt_params.merge("proto" => alt_proto))
89
+ end
90
+ end
91
+
92
+ def parse_altsvc_scheme(alt_proto)
93
+ case alt_proto
94
+ when "h2c"
95
+ "http"
96
+ when "h2"
97
+ "https"
84
98
  end
85
99
  end
86
100
 
87
101
  # :nocov:
88
102
  if RUBY_VERSION < "2.2"
89
- def parse_altsvc_origin(alt_origin)
90
- alt_proto, alt_origin = alt_origin.split("=")
103
+ def parse_altsvc_origin(alt_proto, alt_origin)
104
+ alt_scheme = parse_altsvc_scheme(alt_proto) or return
105
+
91
106
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
92
107
  if alt_origin.start_with?(":")
93
- alt_origin = "#{alt_proto}://dummy#{alt_origin}"
108
+ alt_origin = "#{alt_scheme}://dummy#{alt_origin}"
94
109
  uri = URI.parse(alt_origin)
95
110
  uri.host = nil
96
111
  uri
97
112
  else
98
- URI.parse("#{alt_proto}://#{alt_origin}")
113
+ URI.parse("#{alt_scheme}://#{alt_origin}")
99
114
  end
100
115
  end
101
116
  else
102
- def parse_altsvc_origin(alt_origin)
103
- alt_proto, alt_origin = alt_origin.split("=")
117
+ def parse_altsvc_origin(alt_proto, alt_origin)
118
+ alt_scheme = parse_altsvc_scheme(alt_proto) or return
104
119
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
105
- URI.parse("#{alt_proto}://#{alt_origin}")
120
+
121
+ URI.parse("#{alt_scheme}://#{alt_origin}")
106
122
  end
107
123
  end
108
124
  # :nocov:
@@ -184,7 +184,7 @@ module HTTPX
184
184
  end
185
185
 
186
186
  if @pipelining
187
- disable
187
+ catch(:called) { disable }
188
188
  else
189
189
  @requests.each do |request|
190
190
  emit(:error, request, ex)
@@ -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,13 +209,19 @@ 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
220
216
 
221
217
  def join_headers(stream, request)
222
218
  extra_headers = set_protocol_headers(request)
219
+
220
+ if request.headers.key?("host")
221
+ log { "forbidden \"host\" header found (#{request.headers["host"]}), will use it as authority..." }
222
+ extra_headers[":authority"] = request.headers["host"]
223
+ end
224
+
223
225
  log(level: 1, color: :yellow) do
224
226
  request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
225
227
  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 :origin, :origins, :state, :pending, :options
48
48
 
49
49
  attr_writer :timers
50
50
 
@@ -489,6 +489,18 @@ module HTTPX
489
489
  end
490
490
 
491
491
  def transition(nextstate)
492
+ handle_transition(nextstate)
493
+ rescue Errno::ECONNREFUSED,
494
+ Errno::EADDRNOTAVAIL,
495
+ Errno::EHOSTUNREACH,
496
+ TLSError => e
497
+ # connect errors, exit gracefully
498
+ handle_error(e)
499
+ @state = :closed
500
+ emit(:close)
501
+ end
502
+
503
+ def handle_transition(nextstate)
492
504
  case nextstate
493
505
  when :idle
494
506
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
@@ -525,14 +537,6 @@ module HTTPX
525
537
  emit(:activate)
526
538
  end
527
539
  @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
540
  end
537
541
 
538
542
  def purge_after_closed
@@ -550,6 +554,10 @@ module HTTPX
550
554
  ex.set_backtrace(error.backtrace)
551
555
  error = ex
552
556
  else
557
+ # inactive connections do not contribute to the select loop, therefore
558
+ # they should fail due to such errors.
559
+ return if @state == :inactive
560
+
553
561
  if @timeout
554
562
  @timeout -= error.timeout
555
563
  return unless @timeout <= 0
data/lib/httpx/io/tcp.rb CHANGED
@@ -57,7 +57,9 @@ module HTTPX
57
57
  @io = build_socket
58
58
  end
59
59
  try_connect
60
- rescue Errno::EHOSTUNREACH => e
60
+ rescue Errno::ECONNREFUSED,
61
+ Errno::EADDRNOTAVAIL,
62
+ Errno::EHOSTUNREACH => e
61
63
  raise e if @ip_index <= 0
62
64
 
63
65
  @ip_index -= 1
data/lib/httpx/io/udp.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
3
  require "ipaddr"
5
4
 
6
5
  module HTTPX
@@ -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
@@ -8,11 +8,25 @@ module HTTPX
8
8
  DEFAULT_MIMETYPE = "application/octet-stream"
9
9
 
10
10
  # inspired by https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/determine_mime_type.rb
11
- if defined?(MIME::Types)
11
+ if defined?(FileMagic)
12
+ MAGIC_NUMBER = 256 * 1024
12
13
 
13
- def call(_file, filename)
14
- mime = MIME::Types.of(filename).first
15
- mime.content_type if mime
14
+ def call(file, _)
15
+ return nil if file.eof? # FileMagic returns "application/x-empty" for empty files
16
+
17
+ mime = FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
18
+ filemagic.buffer(file.read(MAGIC_NUMBER))
19
+ end
20
+
21
+ file.rewind
22
+
23
+ mime
24
+ end
25
+ elsif defined?(Marcel)
26
+ def call(file, filename)
27
+ return nil if file.eof? # marcel returns "application/octet-stream" for empty files
28
+
29
+ Marcel::MimeType.for(file, name: filename)
16
30
  end
17
31
 
18
32
  elsif defined?(MimeMagic)
@@ -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
@@ -85,7 +85,7 @@ module HTTPX
85
85
  end
86
86
  uris
87
87
  end
88
- options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
88
+ { uri: @_proxy_uris.first } unless @_proxy_uris.empty?
89
89
  end
90
90
 
91
91
  def find_connection(request, connections, options)
@@ -109,8 +109,10 @@ module HTTPX
109
109
  return super unless proxy
110
110
 
111
111
  connection = options.connection_class.new("tcp", uri, options)
112
- pool.init_connection(connection, options)
113
- connection
112
+ catch(:coalesced) do
113
+ pool.init_connection(connection, options)
114
+ connection
115
+ end
114
116
  end
115
117
 
116
118
  def fetch_response(request, connections, options)
@@ -138,10 +140,20 @@ module HTTPX
138
140
  error = response.error
139
141
  case error
140
142
  when NativeResolveError
143
+ return false unless @_proxy_uris && !@_proxy_uris.empty?
144
+
145
+ proxy_uri = URI(@_proxy_uris.first)
146
+
147
+ origin = error.connection.origin
148
+
141
149
  # failed resolving proxy domain
142
- error.connection.origin.to_s == @_proxy_uris.first
150
+ origin.host == proxy_uri.host && origin.port == proxy_uri.port
143
151
  when ResolveError
144
- error.message.end_with?(@_proxy_uris.first)
152
+ return false unless @_proxy_uris && !@_proxy_uris.empty?
153
+
154
+ proxy_uri = URI(@_proxy_uris.first)
155
+
156
+ error.message.end_with?(proxy_uri.to_s)
145
157
  when *PROXY_ERRORS
146
158
  # timeout errors connecting to proxy
147
159
  true
@@ -160,7 +172,9 @@ module HTTPX
160
172
 
161
173
  # redefining the connection origin as the proxy's URI,
162
174
  # as this will be used as the tcp peer ip.
163
- @origin = URI(@options.proxy.uri.origin)
175
+ proxy_uri = URI(@options.proxy.uri)
176
+ @origin.host = proxy_uri.host
177
+ @origin.port = proxy_uri.port
164
178
  end
165
179
 
166
180
  def match?(uri, options)
@@ -169,11 +183,20 @@ module HTTPX
169
183
  super && @options.proxy == options.proxy
170
184
  end
171
185
 
172
- # should not coalesce connections here, as the IP is the IP of the proxy
173
- def coalescable?(*)
186
+ def coalescable?(connection)
174
187
  return super unless @options.proxy
175
188
 
176
- false
189
+ if @io.protocol == "h2" &&
190
+ @origin.scheme == "https" &&
191
+ connection.origin.scheme == "https" &&
192
+ @io.can_verify_peer?
193
+ # in proxied connections, .origin is the proxy ; Given names
194
+ # are stored in .origins, this is what is used.
195
+ origin = URI(connection.origins.first)
196
+ @io.verify_hostname(origin.host)
197
+ else
198
+ @origin == connection.origin
199
+ end
177
200
  end
178
201
 
179
202
  def send(request)
@@ -222,7 +245,7 @@ module HTTPX
222
245
  end
223
246
  end
224
247
 
225
- def transition(nextstate)
248
+ def handle_transition(nextstate)
226
249
  return super unless @options.proxy
227
250
 
228
251
  case nextstate
@@ -29,7 +29,7 @@ module HTTPX
29
29
  attr_writer :pool
30
30
 
31
31
  def initialize(options)
32
- @options = Options.new(options)
32
+ @options = HTTPX::Options.new(options)
33
33
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
34
34
  @_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
35
35
  @queries = {}
@@ -50,7 +50,7 @@ module HTTPX
50
50
  attr_reader :state
51
51
 
52
52
  def initialize(options)
53
- @options = Options.new(options)
53
+ @options = HTTPX::Options.new(options)
54
54
  @ns_index = 0
55
55
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
56
56
  @nameserver = @resolver_options[:nameserver]
@@ -200,6 +200,7 @@ module HTTPX
200
200
  hostname, connection = @queries.first
201
201
  if @_record_types[hostname].empty?
202
202
  @queries.delete(hostname)
203
+ @timeouts.delete(hostname)
203
204
  @connections.delete(connection)
204
205
  ex = NativeResolveError.new(connection, hostname, e.message)
205
206
  ex.set_backtrace(e.backtrace)
@@ -213,6 +214,7 @@ module HTTPX
213
214
  if @_record_types[hostname].empty?
214
215
  @queries.delete(hostname)
215
216
  @_record_types.delete(hostname)
217
+ @timeouts.delete(hostname)
216
218
  @connections.delete(connection)
217
219
 
218
220
  raise NativeResolveError.new(connection, hostname)
@@ -236,13 +238,18 @@ module HTTPX
236
238
  end
237
239
 
238
240
  if address.key?("alias") # CNAME
239
- if early_resolve(connection, hostname: address["alias"])
241
+ # clean up intermediate queries
242
+ @timeouts.delete(address["name"]) unless connection.origin.host == address["name"]
243
+
244
+ if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
245
+ @timeouts.delete(connection.origin.host)
240
246
  @connections.delete(connection)
241
247
  else
242
248
  resolve(connection, address["alias"])
243
249
  return
244
250
  end
245
251
  else
252
+ @timeouts.delete(connection.origin.host)
246
253
  @connections.delete(connection)
247
254
  Resolver.cached_lookup_set(connection.origin.host, addresses) if @resolver_options[:cache]
248
255
  emit_addresses(connection, addresses.map { |addr| addr["data"] })
@@ -32,7 +32,7 @@ module HTTPX
32
32
  end
33
33
  log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
34
34
  connection.addresses = addresses
35
- catch(:coalesced) { emit(:resolve, connection) }
35
+ emit(:resolve, connection)
36
36
  end
37
37
 
38
38
  def early_resolve(connection, hostname: connection.origin.host)
@@ -15,7 +15,7 @@ module HTTPX
15
15
  attr_reader :state
16
16
 
17
17
  def initialize(options)
18
- @options = Options.new(options)
18
+ @options = HTTPX::Options.new(options)
19
19
  @resolver_options = @options.resolver_options
20
20
  @state = :idle
21
21
  resolv_options = @resolver_options.dup
data/lib/httpx/session.rb CHANGED
@@ -163,7 +163,7 @@ module HTTPX
163
163
  case uri.scheme
164
164
  when "http"
165
165
  "tcp"
166
- when "https", "h2"
166
+ when "https"
167
167
  "ssl"
168
168
  else
169
169
  raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
@@ -207,7 +207,7 @@ module HTTPX
207
207
 
208
208
  return responses unless request
209
209
 
210
- pool.next_tick until (response = fetch_response(request, connections, request.options))
210
+ catch(:coalesced) { pool.next_tick } until (response = fetch_response(request, connections, request.options))
211
211
 
212
212
  responses << response
213
213
  requests.shift
@@ -309,18 +309,4 @@ module HTTPX
309
309
  # :nocov:
310
310
  end
311
311
  end
312
-
313
- unless ENV.grep(/https?_proxy$/i).empty?
314
- proxy_session = plugin(:proxy)
315
- ::HTTPX.send(:remove_const, :Session)
316
- ::HTTPX.send(:const_set, :Session, proxy_session.class)
317
- end
318
-
319
- # :nocov:
320
- if Session.default_options.debug_level > 2
321
- proxy_session = plugin(:internal_telemetry)
322
- ::HTTPX.send(:remove_const, :Session)
323
- ::HTTPX.send(:const_set, :Session, proxy_session.class)
324
- end
325
- # :nocov:
326
312
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ unless ENV.keys.grep(/\Ahttps?_proxy\z/i).empty?
5
+ proxy_session = plugin(:proxy)
6
+ remove_const(:Session)
7
+ const_set(:Session, proxy_session.class)
8
+ remove_const(:Options)
9
+ const_set(:Options, proxy_session.class.default_options.class)
10
+ end
11
+
12
+ # :nocov:
13
+ if Session.default_options.debug_level > 2
14
+ proxy_session = plugin(:internal_telemetry)
15
+ remove_const(:Session)
16
+ const_set(:Session, proxy_session.class)
17
+ end
18
+ # :nocov:
19
+ end
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.18.3"
4
+ VERSION = "0.18.7"
5
5
  end
data/lib/httpx.rb CHANGED
@@ -64,3 +64,4 @@ module HTTPX
64
64
  end
65
65
 
66
66
  require "httpx/session"
67
+ require "httpx/session_extensions"
@@ -67,8 +67,6 @@ module HTTPX
67
67
 
68
68
  def set_protocol_headers: (Request) -> _Each[[String, String]]
69
69
 
70
- def headline_uri: (Request) -> String
71
-
72
70
  def handle: (Request request) -> void
73
71
 
74
72
  def join_headers: (Request request) -> void
@@ -46,8 +46,6 @@ module HTTPX
46
46
 
47
47
  def send_pending: () -> void
48
48
 
49
- def headline_uri: (Request) -> String
50
-
51
49
  def set_protocol_headers: (Request) -> _Each[[String, String]]
52
50
 
53
51
  def handle: (Request request, HTTP2Next::Stream stream) -> void
@@ -56,6 +54,8 @@ module HTTPX
56
54
 
57
55
  def handle_stream: (HTTP2Next::Stream stream, Request request) -> void
58
56
 
57
+ def join_headline: (Request request) -> String
58
+
59
59
  def join_headers: (HTTP2Next::Stream stream, Request request) -> void
60
60
 
61
61
  def join_trailers: (HTTP2Next::Stream stream, Request request) -> void
data/sig/connection.rbs CHANGED
@@ -22,6 +22,7 @@ module HTTPX
22
22
  BUFFER_SIZE: Integer
23
23
 
24
24
  attr_reader origin: URI::Generic
25
+ attr_reader origins: Array[String]
25
26
  attr_reader state: Symbol
26
27
  attr_reader pending: Array[Request]
27
28
  attr_reader options: Options
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.3
4
+ version: 0.18.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-14 00:00:00.000000000 Z
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2-next
@@ -66,6 +66,10 @@ extra_rdoc_files:
66
66
  - doc/release_notes/0_18_1.md
67
67
  - doc/release_notes/0_18_2.md
68
68
  - doc/release_notes/0_18_3.md
69
+ - doc/release_notes/0_18_4.md
70
+ - doc/release_notes/0_18_5.md
71
+ - doc/release_notes/0_18_6.md
72
+ - doc/release_notes/0_18_7.md
69
73
  - doc/release_notes/0_1_0.md
70
74
  - doc/release_notes/0_2_0.md
71
75
  - doc/release_notes/0_2_1.md
@@ -125,6 +129,10 @@ files:
125
129
  - doc/release_notes/0_18_1.md
126
130
  - doc/release_notes/0_18_2.md
127
131
  - doc/release_notes/0_18_3.md
132
+ - doc/release_notes/0_18_4.md
133
+ - doc/release_notes/0_18_5.md
134
+ - doc/release_notes/0_18_6.md
135
+ - doc/release_notes/0_18_7.md
128
136
  - doc/release_notes/0_1_0.md
129
137
  - doc/release_notes/0_2_0.md
130
138
  - doc/release_notes/0_2_1.md
@@ -228,6 +236,7 @@ files:
228
236
  - lib/httpx/selector.rb
229
237
  - lib/httpx/session.rb
230
238
  - lib/httpx/session2.rb
239
+ - lib/httpx/session_extensions.rb
231
240
  - lib/httpx/timers.rb
232
241
  - lib/httpx/transcoder.rb
233
242
  - lib/httpx/transcoder/body.rb
@@ -322,7 +331,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
322
331
  - !ruby/object:Gem::Version
323
332
  version: '0'
324
333
  requirements: []
325
- rubygems_version: 3.2.22
334
+ rubygems_version: 3.2.32
326
335
  signing_key:
327
336
  specification_version: 4
328
337
  summary: HTTPX, to the future, and beyond