httpx 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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