httpx 1.1.2 → 1.1.3

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: 118700ac0c350952382970ddb1ed33d74d954e1ddd6c2f82bb032cc24a3a5d22
4
- data.tar.gz: 160644aeefb2ed61a8bda67b7896bbd2a4a4211cf9283ebe4e9a244ac237e780
3
+ metadata.gz: 88e35920c570a12032835c1c67c9ffde6119b0f5e26e357ba58dbe385e1483a5
4
+ data.tar.gz: e421a0532b92e6a1e689c1d907ef0b87d98e6589093d791ce9a905d475b0c638
5
5
  SHA512:
6
- metadata.gz: 97d15923bd32a0378bcf54a147b37b308eda78a74f67f339467dd3130cffd0aab7dace5bb368dbf9f5a203f6bc73ea37db394e153107daaf3c81b389f269d849
7
- data.tar.gz: 16e71f8eb94de6314a77b96620be5db341afa81f2fb026780bd467fa0aaa9aa3d395d3afca5f59ebbd3aae507b33c88d22a4c864e40bfa4b23031162a365bcb8
6
+ metadata.gz: e01adb8c3974497b091c72d8b9848dd02973c19924c910be4649f83c3430b579a6ea0889f0f27a348cabbddd6ed56cc1b682be0ae984b4dd0ef5dbf76543ba8e
7
+ data.tar.gz: 343ace24f2a3be6ce420b4c5f8fbe919ad84dabdc8f3b97bbafc3bda8466fef6dfa48a1985478fd7ec6056678818a60b9cfb158f4423569086f42d4c27c696f6
data/README.md CHANGED
@@ -46,7 +46,7 @@ And that's the simplest one there is. But you can also do:
46
46
  HTTPX.post("http://example.com", form: { user: "john", password: "pass" })
47
47
 
48
48
  http = HTTPX.with(headers: { "x-my-name" => "joe" })
49
- http.patch(("http://example.com/file", body: File.open("path/to/file")) # request body is streamed
49
+ http.patch("http://example.com/file", body: File.open("path/to/file")) # request body is streamed
50
50
  ```
51
51
 
52
52
  If you want to do some more things with the response, you can get an `HTTPX::Response`:
@@ -151,7 +151,7 @@ All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
151
151
 
152
152
  ## Versioning Policy
153
153
 
154
- 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.
154
+ `httpx` follows Semantic Versioning.
155
155
 
156
156
  ## Contributing
157
157
 
@@ -0,0 +1,18 @@
1
+ # 1.1.2
2
+
3
+ ## improvements
4
+
5
+ ## security
6
+
7
+ * when using `:follow_redirects` plugin, the "authorization" header will be removed when following redirect responses to a different origin.
8
+
9
+ ## bugfixes
10
+
11
+ * fixed `:stream` plugin not following redirect responses when used with the `:follow_redirects` plugin.
12
+ * fixed `:stream` plugin not doing content decoding when responses were p.ex. gzip-compressed.
13
+ * fixed bug preventing usage of IPv6 loopback or link-local addresses in the request URL in systems with no IPv6 internet connectivity (the request was left hanging).
14
+ * protect all code which may initiate a new connection from abrupt errors (such as internet turned off), as it was done on the initial request call.
15
+
16
+ ## chore
17
+
18
+ internal usage of `mutex_m` has been removed (`mutex_m` is going to be deprecated in ruby 3.3).
data/lib/httpx/altsvc.rb CHANGED
@@ -4,7 +4,7 @@ require "strscan"
4
4
 
5
5
  module HTTPX
6
6
  module AltSvc
7
- @altsvc_mutex = Mutex.new
7
+ @altsvc_mutex = Thread::Mutex.new
8
8
  @altsvcs = Hash.new { |h, k| h[k] = [] }
9
9
 
10
10
  module_function
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mutex_m"
4
-
5
3
  module HTTPX::Plugins::CircuitBreaker
6
4
  using HTTPX::URIExtensions
7
5
 
@@ -15,17 +13,17 @@ module HTTPX::Plugins::CircuitBreaker
15
13
  options.circuit_breaker_half_open_drip_rate
16
14
  )
17
15
  end
18
- @circuits.extend(Mutex_m)
16
+ @circuits_mutex = Thread::Mutex.new
19
17
  end
20
18
 
21
19
  def try_open(uri, response)
22
- circuit = @circuits.synchronize { get_circuit_for_uri(uri) }
20
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(uri) }
23
21
 
24
22
  circuit.try_open(response)
25
23
  end
26
24
 
27
25
  def try_close(uri)
28
- circuit = @circuits.synchronize do
26
+ circuit = @circuits_mutex.synchronize do
29
27
  return unless @circuits.key?(uri.origin) || @circuits.key?(uri.to_s)
30
28
 
31
29
  get_circuit_for_uri(uri)
@@ -37,7 +35,7 @@ module HTTPX::Plugins::CircuitBreaker
37
35
  # if circuit is open, it'll respond with the stored response.
38
36
  # if not, nil.
39
37
  def try_respond(request)
40
- circuit = @circuits.synchronize { get_circuit_for_uri(request.uri) }
38
+ circuit = @circuits_mutex.synchronize { get_circuit_for_uri(request.uri) }
41
39
 
42
40
  circuit.respond
43
41
  end
@@ -99,8 +99,7 @@ module HTTPX
99
99
  response.close
100
100
  request.headers.delete("expect")
101
101
  request.transition(:idle)
102
- connection = find_connection(request, connections, options)
103
- connection.send(request)
102
+ send_request(request, connections, options)
104
103
  return
105
104
  end
106
105
 
@@ -17,6 +17,8 @@ module HTTPX
17
17
  MAX_REDIRECTS = 3
18
18
  REDIRECT_STATUS = (300..399).freeze
19
19
 
20
+ using URIExtensions
21
+
20
22
  module OptionsMethods
21
23
  def option_max_redirects(value)
22
24
  num = Integer(value)
@@ -28,6 +30,10 @@ module HTTPX
28
30
  def option_follow_insecure_redirects(value)
29
31
  value
30
32
  end
33
+
34
+ def option_allow_auth_to_other_origins(value)
35
+ value
36
+ end
31
37
  end
32
38
 
33
39
  module InstanceMethods
@@ -61,25 +67,31 @@ module HTTPX
61
67
  redirect_uri = redirect_request.uri
62
68
  options = retry_options
63
69
  else
70
+ redirect_headers = redirect_request_headers(redirect_request.uri, redirect_uri, request.headers, options)
64
71
 
65
72
  # redirects are **ALWAYS** GET
66
- retry_options = options.merge(headers: redirect_request.headers,
67
- body: redirect_request.body,
68
- max_redirects: max_redirects - 1)
73
+ retry_opts = Hash[options].merge(
74
+ headers: redirect_headers.to_h,
75
+ body: redirect_request.body,
76
+ max_redirects: max_redirects - 1
77
+ )
78
+ retry_options = options.class.new(retry_opts)
69
79
  end
70
80
 
71
- retry_request = build_request("GET", redirect_uri, retry_options)
72
-
73
- request.redirect_request = retry_request
81
+ redirect_uri = Utils.to_uri(redirect_uri)
74
82
 
75
83
  if !options.follow_insecure_redirects &&
76
84
  response.uri.scheme == "https" &&
77
- retry_request.uri.scheme == "http"
78
- error = InsecureRedirectError.new(retry_request.uri.to_s)
85
+ redirect_uri.scheme == "http"
86
+ error = InsecureRedirectError.new(redirect_uri.to_s)
79
87
  error.set_backtrace(caller)
80
88
  return ErrorResponse.new(request, error, options)
81
89
  end
82
90
 
91
+ retry_request = build_request("GET", redirect_uri, retry_options)
92
+
93
+ request.redirect_request = retry_request
94
+
83
95
  retry_after = response.headers["retry-after"]
84
96
 
85
97
  if retry_after
@@ -93,16 +105,27 @@ module HTTPX
93
105
 
94
106
  log { "redirecting after #{retry_after} secs..." }
95
107
  pool.after(retry_after) do
96
- connection = find_connection(retry_request, connections, options)
97
- connection.send(retry_request)
108
+ send_request(retry_request, connections, options)
98
109
  end
99
110
  else
100
- connection = find_connection(retry_request, connections, options)
101
- connection.send(retry_request)
111
+ send_request(retry_request, connections, options)
102
112
  end
103
113
  nil
104
114
  end
105
115
 
116
+ def redirect_request_headers(original_uri, redirect_uri, headers, options)
117
+ return headers if options.allow_auth_to_other_origins
118
+
119
+ return headers unless headers.key?("authorization")
120
+
121
+ unless original_uri.origin == redirect_uri.origin
122
+ headers = headers.dup
123
+ headers.delete("authorization")
124
+ end
125
+
126
+ headers
127
+ end
128
+
106
129
  def __get_location_from_response(response)
107
130
  location_uri = URI(response.headers["location"])
108
131
  location_uri = response.uri.merge(location_uri) if location_uri.relative?
@@ -111,14 +134,18 @@ module HTTPX
111
134
  end
112
135
 
113
136
  module RequestMethods
114
- def self.included(klass)
115
- klass.__send__(:attr_writer, :redirect_request)
116
- end
137
+ attr_accessor :root_request
117
138
 
118
139
  def redirect_request
119
140
  @redirect_request || self
120
141
  end
121
142
 
143
+ def redirect_request=(req)
144
+ @redirect_request = req
145
+ req.root_request = @root_request || self
146
+ @response = nil
147
+ end
148
+
122
149
  def response
123
150
  return super unless @redirect_request
124
151
 
@@ -38,7 +38,7 @@ module HTTPX
38
38
  request.transition(:idle)
39
39
  request.headers["proxy-authorization"] =
40
40
  connection.options.proxy.authenticate(request, response.headers["proxy-authenticate"])
41
- connection.send(request)
41
+ send_request(request, connections)
42
42
  return
43
43
  end
44
44
  end
@@ -166,9 +166,7 @@ module HTTPX
166
166
  @_proxy_uris.shift
167
167
  log { "failed connecting to proxy, trying next..." }
168
168
  request.transition(:idle)
169
- connection = find_connection(request, connections, options)
170
- connections << connection unless connections.include?(connection)
171
- connection.send(request)
169
+ send_request(request, connections, options)
172
170
  return
173
171
  end
174
172
  response
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mutex_m"
4
-
5
3
  module HTTPX::Plugins
6
4
  module ResponseCache
7
5
  class Store
8
6
  def initialize
9
7
  @store = {}
10
- @store.extend(Mutex_m)
8
+ @store_mutex = Thread::Mutex.new
11
9
  end
12
10
 
13
11
  def clear
14
- @store.synchronize { @store.clear }
12
+ @store_mutex.synchronize { @store.clear }
15
13
  end
16
14
 
17
15
  def lookup(request)
@@ -66,7 +64,7 @@ module HTTPX::Plugins
66
64
  end
67
65
 
68
66
  def _get(request)
69
- @store.synchronize do
67
+ @store_mutex.synchronize do
70
68
  responses = @store[request.response_cache_key]
71
69
 
72
70
  return unless responses
@@ -80,7 +78,7 @@ module HTTPX::Plugins
80
78
  end
81
79
 
82
80
  def _set(request, response)
83
- @store.synchronize do
81
+ @store_mutex.synchronize do
84
82
  responses = (@store[request.response_cache_key] ||= [])
85
83
 
86
84
  responses.reject! do |res|
@@ -113,12 +113,10 @@ module HTTPX
113
113
  log { "retrying after #{retry_after} secs..." }
114
114
  pool.after(retry_after) do
115
115
  log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
116
- connection = find_connection(request, connections, options)
117
- connection.send(request)
116
+ send_request(request, connections, options)
118
117
  end
119
118
  else
120
- connection = find_connection(request, connections, options)
121
- connection.send(request)
119
+ send_request(request, connections, options)
122
120
  end
123
121
 
124
122
  return
@@ -11,8 +11,6 @@ module HTTPX
11
11
  def each(&block)
12
12
  return enum_for(__method__) unless block
13
13
 
14
- raise Error, "response already streamed" if @response
15
-
16
14
  @request.stream = self
17
15
 
18
16
  begin
@@ -119,7 +117,10 @@ module HTTPX
119
117
 
120
118
  module ResponseMethods
121
119
  def stream
122
- @request.stream
120
+ request = @request.root_request if @request.respond_to?(:root_request)
121
+ request ||= @request
122
+
123
+ request.stream
123
124
  end
124
125
  end
125
126
 
@@ -132,7 +133,13 @@ module HTTPX
132
133
  def write(chunk)
133
134
  return super unless @stream
134
135
 
135
- @stream.on_chunk(chunk.to_s.dup)
136
+ return 0 if chunk.empty?
137
+
138
+ chunk = decode_chunk(chunk)
139
+
140
+ @stream.on_chunk(chunk.dup)
141
+
142
+ chunk.size
136
143
  end
137
144
 
138
145
  private
@@ -46,7 +46,6 @@ module HTTPX
46
46
 
47
47
  log { "upgrading to #{upgrade_protocol}..." }
48
48
  connection = find_connection(request, connections, options)
49
- connections << connection unless connections.include?(connection)
50
49
 
51
50
  # do not upgrade already upgraded connections
52
51
  return if connection.upgrade_protocol == upgrade_protocol
@@ -46,14 +46,15 @@ module HTTPX
46
46
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
47
  return unless addresses
48
48
 
49
- addresses = addresses.group_by(&:family)
49
+ addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
50
+ # try to match the resolver by family. However, there are cases where that's not possible, as when
51
+ # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
52
+ resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
50
53
 
51
- @resolvers.each do |resolver|
52
- addrs = addresses[resolver.family]
54
+ next unless resolver # this should ever happen
53
55
 
54
- next if !addrs || addrs.empty?
55
-
56
- resolver.emit_addresses(connection, resolver.family, addrs, true)
56
+ # it does not matter which resolver it is, as early-resolve code is shared.
57
+ resolver.emit_addresses(connection, family, addrs, true)
57
58
  end
58
59
  end
59
60
 
@@ -13,10 +13,10 @@ module HTTPX
13
13
  require "httpx/resolver/https"
14
14
  require "httpx/resolver/multi"
15
15
 
16
- @lookup_mutex = Mutex.new
16
+ @lookup_mutex = Thread::Mutex.new
17
17
  @lookups = Hash.new { |h, k| h[k] = [] }
18
18
 
19
- @identifier_mutex = Mutex.new
19
+ @identifier_mutex = Thread::Mutex.new
20
20
  @identifier = 1
21
21
  @system_resolver = Resolv::Hosts.new
22
22
 
@@ -41,9 +41,9 @@ module HTTPX
41
41
  def write(chunk)
42
42
  return if @state == :closed
43
43
 
44
- @inflaters.reverse_each do |inflater|
45
- chunk = inflater.call(chunk)
46
- end if @inflaters && !chunk.empty?
44
+ return 0 if chunk.empty?
45
+
46
+ chunk = decode_chunk(chunk)
47
47
 
48
48
  size = chunk.bytesize
49
49
  @length += size
@@ -187,6 +187,14 @@ module HTTPX
187
187
  end
188
188
  end
189
189
 
190
+ def decode_chunk(chunk)
191
+ @inflaters.reverse_each do |inflater|
192
+ chunk = inflater.call(chunk)
193
+ end if @inflaters
194
+
195
+ chunk
196
+ end
197
+
190
198
  def transition(nextstate)
191
199
  case nextstate
192
200
  when :open
data/lib/httpx/session.rb CHANGED
@@ -151,6 +151,16 @@ module HTTPX
151
151
  connection
152
152
  end
153
153
 
154
+ def send_request(request, connections, options = request.options)
155
+ error = catch(:resolve_error) do
156
+ connection = find_connection(request, connections, options)
157
+ connection.send(request)
158
+ end
159
+ return unless error.is_a?(Error)
160
+
161
+ request.emit(:response, ErrorResponse.new(request, error, options))
162
+ end
163
+
154
164
  # sets the callbacks on the +connection+ required to process certain specific
155
165
  # connection lifecycle events which deal with request rerouting.
156
166
  def set_connection_callbacks(connection, connections, options)
@@ -281,13 +291,7 @@ module HTTPX
281
291
  connections = []
282
292
 
283
293
  requests.each do |request|
284
- error = catch(:resolve_error) do
285
- connection = find_connection(request, connections, request.options)
286
- connection.send(request)
287
- end
288
- next unless error.is_a?(ResolveError)
289
-
290
- request.emit(:response, ErrorResponse.new(request, error, request.options))
294
+ send_request(request, connections)
291
295
  end
292
296
 
293
297
  connections
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.1.2"
4
+ VERSION = "1.1.3"
5
5
  end
data/lib/httpx.rb CHANGED
@@ -20,7 +20,6 @@ require "httpx/response"
20
20
  require "httpx/options"
21
21
  require "httpx/chainable"
22
22
 
23
- require "mutex_m"
24
23
  # Top-Level Namespace
25
24
  #
26
25
  module HTTPX
@@ -31,16 +30,17 @@ module HTTPX
31
30
  #
32
31
  module Plugins
33
32
  @plugins = {}
34
- @plugins.extend(Mutex_m)
33
+ @plugins_mutex = Thread::Mutex.new
35
34
 
36
35
  # Loads a plugin based on a name. If the plugin hasn't been loaded, tries to load
37
36
  # it from the load path under "httpx/plugins/" directory.
38
37
  #
39
38
  def self.load_plugin(name)
40
39
  h = @plugins
41
- unless (plugin = h.synchronize { h[name] })
40
+ m = @plugins_mutex
41
+ unless (plugin = m.synchronize { h[name] })
42
42
  require "httpx/plugins/#{name}"
43
- raise "Plugin #{name} hasn't been registered" unless (plugin = h.synchronize { h[name] })
43
+ raise "Plugin #{name} hasn't been registered" unless (plugin = m.synchronize { h[name] })
44
44
  end
45
45
  plugin
46
46
  end
@@ -49,7 +49,8 @@ module HTTPX
49
49
  #
50
50
  def self.register_plugin(name, mod)
51
51
  h = @plugins
52
- h.synchronize { h[name] = mod }
52
+ m = @plugins_mutex
53
+ m.synchronize { h[name] = mod }
53
54
  end
54
55
  end
55
56
 
@@ -3,7 +3,9 @@ module HTTPX
3
3
  module CircuitBreaker
4
4
 
5
5
  class CircuitStore
6
- @circuits: Hash[String, Circuit] & Mutex_m
6
+ @circuits: Hash[String, Circuit]
7
+
8
+ @circuits_mutex: Thread::Mutex
7
9
 
8
10
  def try_open: (uri uri, response response) -> response?
9
11
 
@@ -10,6 +10,8 @@ module HTTPX
10
10
  def max_redirects: () -> Integer?
11
11
 
12
12
  def follow_insecure_redirects: () -> bool?
13
+
14
+ def allow_auth_to_other_origins: () -> bool?
13
15
  end
14
16
 
15
17
  def self.extra_options: (Options) -> (Options & _FollowRedirectsOptions)
@@ -17,12 +19,18 @@ module HTTPX
17
19
  module InstanceMethods
18
20
  def max_redirects: (_ToI) -> instance
19
21
 
22
+ def redirect_request_headers: (http_uri original_uri, http_uri redirect_uri, Headers headers, Options & _FollowRedirectsOptions options) -> Headers
23
+
20
24
  def __get_location_from_response: (Response) -> (URI::HTTP | URI::HTTPS)
21
25
  end
22
26
 
23
27
  module RequestMethods
24
- def redirect_request: () -> Request
28
+ attr_accessor root_request: instance?
29
+
30
+ def redirect_request: () -> instance
31
+
25
32
  def redirect_request=: (Request) -> void
33
+
26
34
  def max_redirects: () -> Integer
27
35
  end
28
36
  end
@@ -9,7 +9,9 @@ module HTTPX
9
9
  def self?.cached_response?: (response response) -> bool
10
10
 
11
11
  class Store
12
- @store: Hash[String, Array[Response]] & Mutex_m
12
+ @store: Hash[String, Array[Response]]
13
+
14
+ @store_mutex: Thread::Mutex
13
15
 
14
16
  def lookup: (Request request) -> Response?
15
17
 
@@ -44,6 +44,8 @@ module HTTPX
44
44
 
45
45
  def self.initialize_inflater_by_encoding: (Encoding | String encoding, Response response, ?bytesize: Integer) -> Transcoder::GZIP::Inflater
46
46
 
47
+ def decode_chunk: (String chunk) -> String
48
+
47
49
  def transition: (Symbol nextstate) -> void
48
50
 
49
51
  def _with_same_buffer_pos: [A] () { () -> A } -> A
data/sig/session.rbs CHANGED
@@ -17,10 +17,10 @@ module HTTPX
17
17
 
18
18
  def build_request: (verb, generic_uri, ?options) -> Request
19
19
 
20
- private
20
+ def initialize: (?options) { (self) -> void } -> void
21
+ | (?options) -> void
21
22
 
22
- def initialize: (?options) { (self) -> void } -> untyped
23
- | (?options) -> untyped
23
+ private
24
24
 
25
25
  def pool: -> Pool
26
26
  def on_response: (Request, response) -> void
@@ -29,6 +29,8 @@ module HTTPX
29
29
 
30
30
  def find_connection: (Request request, Array[Connection] connections, Options options) -> Connection
31
31
 
32
+ def send_request: (Request request, Array[Connection] connections, ?Options options) -> void
33
+
32
34
  def set_connection_callbacks: (Connection connection, Array[Connection] connections, Options options) -> void
33
35
 
34
36
  def build_altsvc_connection: (Connection existing_connection, Array[Connection] connections, URI::Generic alt_origin, String origin, Hash[String, String] alt_params, Options options) -> Connection?
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: 1.1.2
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-14 00:00:00.000000000 Z
11
+ date: 2023-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2-next
@@ -134,6 +134,7 @@ extra_rdoc_files:
134
134
  - doc/release_notes/1_1_0.md
135
135
  - doc/release_notes/1_1_1.md
136
136
  - doc/release_notes/1_1_2.md
137
+ - doc/release_notes/1_1_3.md
137
138
  files:
138
139
  - LICENSE.txt
139
140
  - README.md
@@ -239,6 +240,7 @@ files:
239
240
  - doc/release_notes/1_1_0.md
240
241
  - doc/release_notes/1_1_1.md
241
242
  - doc/release_notes/1_1_2.md
243
+ - doc/release_notes/1_1_3.md
242
244
  - lib/httpx.rb
243
245
  - lib/httpx/adapters/datadog.rb
244
246
  - lib/httpx/adapters/faraday.rb