httpx 0.23.4 → 0.24.1

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: e3869ae05ccfa2903477391c3ed918d9022ebd628795a09ad9681511a2d46f77
4
- data.tar.gz: c7d296cdbaed94327582b409463677ad549038d5785d8e296e837c243183bf43
3
+ metadata.gz: 588deec0af9ead32480f7cca9ce8c278b344ccdd2e608cc2a6a8db8c6135647f
4
+ data.tar.gz: 3b69af49050cd6ba7b0317ddcd2286cbe6a2261add9fd0b9839bb89290e3962a
5
5
  SHA512:
6
- metadata.gz: 80b74ee360f1db9632a0e1317e3a3ed5ae3ac863e5c6c637e4ba85719027e68bf1e3fcc2103b35b1fc11221bd832af911d985a7dd51eb9030cd9392bf310b591
7
- data.tar.gz: b424d2a3e8c796cd04747bd446a06ee690f1fa5965f397b4e7ab98880ca2d34f97d8846ecccf6973b42351b15543d610dac6ca8217ba68fb53388610071e17e1
6
+ metadata.gz: e630447fb6bc2aabb6efd842fdd322734e78e7859ea03126ab3d45cf4580d91ede945aa4e77700ae6cbe92179cc0a7e96fdc294ccaf729b287c4b4269b9b40b8
7
+ data.tar.gz: 138310fead4041c202f32b3e53564023472383f5cc251c59cc88b115c8f2947801da8269518d3dce48ff8b53abe3b1350d92b553a9264be8ab052064a135b605
@@ -0,0 +1,48 @@
1
+ # 0.24.0
2
+
3
+ ## Features
4
+
5
+ ### `:oauth` plugin
6
+
7
+ The `:oauth` plugin manages the handling of a given OAuth session, in that it ships with convenience methods to generate a new access token, which it then injects in all requests.
8
+
9
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/OAuth
10
+
11
+ ### session callbacks
12
+
13
+ HTTP request/response lifecycle events have now the ability of being intercepted via public API callback methods:
14
+
15
+ ```ruby
16
+ HTTPX.on_request_completed do |request|
17
+ puts "request to #{request.uri} sent"
18
+ end.get(...)
19
+ ```
20
+
21
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/Events to know which events and callback methods are supported.
22
+
23
+ ### `:circuit_breaker` plugin `on_circuit_open` callback
24
+
25
+ A callback has been introduced for the `:circuit_breaker` plugin, which is triggered when a circuit is opened.
26
+
27
+ ```ruby
28
+ http = HTTPX.plugin(:circuit_breaker).on_circuit_open do |req|
29
+ puts "circuit opened for #{req.uri}"
30
+ end
31
+ http.get(...)
32
+ ```
33
+
34
+ ## Improvements
35
+
36
+ Several `:response_cache` features have been improved:
37
+
38
+ * `:response_cache` plugin: response cache store has been made thread-safe.
39
+ * cached response sharing across threads is made safer, as stringio/tempfile instances are copied instead of shared (without copying the underling string/file).
40
+ * stale cached responses are eliminate on cache store lookup/store operations.
41
+ * already closed responses are evicted from the cache store.
42
+ * fallback for lack of compatible response "date" header has been fixed to return a `Time` object.
43
+
44
+ ## Bugfixes
45
+
46
+ * Ability to recover from errors happening during response chunk processing (required for overriding behaviour and response chunk callbacks); error bubbling up will result in the connection being closed.
47
+ * Happy eyeballs support for multi-homed early-resolved domain names (such as `localhost` under `/etc/hosts`) was broken, as it would try the first given IP; so, if given `::1` and connection would fail, it wouldn't try `127.0.0.1`, which would have succeeded.
48
+ * `:digest_authentication` plugin was removing the "algorithm" header on `-sess` declared algorithms, which is required for HTTP digest auth negotiation.
@@ -0,0 +1,12 @@
1
+ # 0.24.1
2
+
3
+ ## Improvements
4
+
5
+ * datadog adapter: support `:service_name` configuration option.
6
+ * datadog adapter: set `:distributed_tracing` to `true` by default.
7
+ * `:proxy` plugin: when the proxy uri uses an unsupported scheme (i.e.: "scp://125.24.2.1"), a more user friendly error is raised (instead of the previous broken stacktrace).
8
+
9
+ ## Bugfixes
10
+
11
+ * datadog adapter: fix tracing enable call, which was wrongly calling `super`.
12
+ + `:proxy` plugin: fix for bug which was turning off plugins overriding `HTTPX::Connection#send` (such as the datadog adapter).
@@ -160,7 +160,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
160
160
 
161
161
  module RequestMethods
162
162
  def __datadog_enable_trace!
163
- return super if @__datadog_enable_trace
163
+ return if @__datadog_enable_trace
164
164
 
165
165
  RequestTracer.new(self).call
166
166
  @__datadog_enable_trace = true
@@ -203,6 +203,27 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
203
203
  o.lazy
204
204
  end
205
205
 
206
+ if defined?(TRACING_MODULE::Contrib::SpanAttributeSchema)
207
+ option :service_name do |o|
208
+ o.default do
209
+ TRACING_MODULE::Contrib::SpanAttributeSchema.fetch_service_name(
210
+ "DD_TRACE_HTTPX_SERVICE_NAME",
211
+ "httpx"
212
+ )
213
+ end
214
+ o.lazy
215
+ end
216
+ else
217
+ option :service_name do |o|
218
+ o.default do
219
+ ENV.fetch("DD_TRACE_HTTPX_SERVICE_NAME", "httpx")
220
+ end
221
+ o.lazy
222
+ end
223
+ end
224
+
225
+ option :distributed_tracing, default: true
226
+
206
227
  option :error_handler, default: DEFAULT_ERROR_HANDLER
207
228
  end
208
229
  end
@@ -4,6 +4,7 @@ module HTTPX
4
4
  module Callbacks
5
5
  def on(type, &action)
6
6
  callbacks(type) << action
7
+ self
7
8
  end
8
9
 
9
10
  def once(type, &block)
@@ -11,6 +12,7 @@ module HTTPX
11
12
  block.call(*args, &callback)
12
13
  :delete
13
14
  end
15
+ self
14
16
  end
15
17
 
16
18
  def only(type, &block)
@@ -10,6 +10,19 @@ module HTTPX
10
10
  MOD
11
11
  end
12
12
 
13
+ %i[
14
+ connection_opened connection_closed
15
+ request_error
16
+ request_started request_body_chunk request_completed
17
+ response_started response_body_chunk response_completed
18
+ ].each do |meth|
19
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
20
+ def on_#{meth}(&blk) # def on_connection_opened(&blk)
21
+ on(:#{meth}, &blk) # on(:connection_opened, &blk)
22
+ end # end
23
+ MOD
24
+ end
25
+
13
26
  def request(*args, **options)
14
27
  branch(default_options).request(*args, **options)
15
28
  end
@@ -56,6 +69,12 @@ module HTTPX
56
69
  branch(default_options.merge(options), &blk)
57
70
  end
58
71
 
72
+ protected
73
+
74
+ def on(*args, &blk)
75
+ branch(default_options).on(*args, &blk)
76
+ end
77
+
59
78
  private
60
79
 
61
80
  def default_options
@@ -133,33 +133,42 @@ module HTTPX
133
133
  end
134
134
 
135
135
  def on_data(chunk)
136
- return unless @request
136
+ request = @request
137
+
138
+ return unless request
137
139
 
138
140
  log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
139
141
  log(level: 2, color: :green) { "-> #{chunk.inspect}" }
140
- response = @request.response
142
+ response = request.response
141
143
 
142
144
  response << chunk
145
+ rescue StandardError => e
146
+ error_response = ErrorResponse.new(request, e, request.options)
147
+ request.response = error_response
148
+ dispatch
143
149
  end
144
150
 
145
151
  def on_complete
146
- return unless @request
152
+ request = @request
153
+
154
+ return unless request
147
155
 
148
156
  log(level: 2) { "parsing complete" }
149
157
  dispatch
150
158
  end
151
159
 
152
160
  def dispatch
153
- if @request.expects?
161
+ request = @request
162
+
163
+ if request.expects?
154
164
  @parser.reset!
155
- return handle(@request)
165
+ return handle(request)
156
166
  end
157
167
 
158
- request = @request
159
168
  @request = nil
160
169
  @requests.shift
161
170
  response = request.response
162
- response.finish!
171
+ response.finish! unless response.is_a?(ErrorResponse)
163
172
  emit(:response, request, response)
164
173
 
165
174
  if @parser.upgrade?
@@ -169,7 +178,11 @@ module HTTPX
169
178
 
170
179
  @parser.reset!
171
180
  @max_requests -= 1
172
- manage_connection(response)
181
+ if response.is_a?(ErrorResponse)
182
+ disable
183
+ else
184
+ manage_connection(response)
185
+ end
173
186
 
174
187
  send(@pending.shift) unless @pending.empty?
175
188
  end
@@ -314,6 +314,7 @@ module HTTPX
314
314
  ex = Error.new(stream.id, error)
315
315
  ex.set_backtrace(caller)
316
316
  response = ErrorResponse.new(request, ex, request.options)
317
+ request.response = response
317
318
  emit(:response, request, response)
318
319
  else
319
320
  response = request.response
@@ -63,7 +63,7 @@ module HTTPX
63
63
  # Normalizes a _domain_ using the Punycode algorithm as necessary.
64
64
  # The result will be a downcased, ASCII-only string.
65
65
  def normalize(domain)
66
- domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
66
+ domain = domain.chomp(DOT).unicode_normalize(:nfc) unless domain.ascii_only?
67
67
  Punycode.encode_hostname(domain).downcase
68
68
  end
69
69
  end
data/lib/httpx/io/tcp.rb CHANGED
@@ -38,7 +38,10 @@ module HTTPX
38
38
  add_addresses(addresses)
39
39
  end
40
40
  @ip_index = @addresses.size - 1
41
- # @io ||= build_socket
41
+ end
42
+
43
+ def socket
44
+ @io.to_io
42
45
  end
43
46
 
44
47
  def add_addresses(addrs)
@@ -45,7 +45,6 @@ module HTTPX
45
45
  raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
46
46
 
47
47
  sess = Regexp.last_match(2)
48
- params.delete("algorithm")
49
48
  else
50
49
  algorithm = ::Digest::MD5
51
50
  end
@@ -77,11 +76,11 @@ module HTTPX
77
76
  %(response="#{algorithm.hexdigest(request_digest)}"),
78
77
  ]
79
78
  header << %(realm="#{params["realm"]}") if params.key?("realm")
80
- header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
81
- header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
79
+ header << %(algorithm=#{params["algorithm"]}) if params.key?("algorithm")
82
80
  header << %(cnonce="#{cnonce}") if cnonce
83
81
  header << %(nc=#{nc})
84
82
  header << %(qop=#{qop}) if qop
83
+ header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
85
84
  header.join ", "
86
85
  end
87
86
 
@@ -31,6 +31,16 @@ module HTTPX
31
31
  @circuit_store = orig.instance_variable_get(:@circuit_store).dup
32
32
  end
33
33
 
34
+ %i[circuit_open].each do |meth|
35
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
36
+ def on_#{meth}(&blk) # def on_circuit_open(&blk)
37
+ on(:#{meth}, &blk) # on(:circuit_open, &blk)
38
+ end # end
39
+ MOD
40
+ end
41
+
42
+ private
43
+
34
44
  def send_requests(*requests)
35
45
  # @type var short_circuit_responses: Array[response]
36
46
  short_circuit_responses = []
@@ -59,6 +69,12 @@ module HTTPX
59
69
  end
60
70
 
61
71
  def on_response(request, response)
72
+ emit(:circuit_open, request) if try_circuit_open(request, response)
73
+
74
+ super
75
+ end
76
+
77
+ def try_circuit_open(request, response)
62
78
  if response.is_a?(ErrorResponse)
63
79
  case response.error
64
80
  when RequestTimeoutError
@@ -69,8 +85,6 @@ module HTTPX
69
85
  elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
70
86
  @circuit_store.try_open(request.uri, response)
71
87
  end
72
-
73
- super
74
88
  end
75
89
  end
76
90
 
@@ -22,7 +22,7 @@ module HTTPX
22
22
 
23
23
  module OptionsMethods
24
24
  def option_digest(value)
25
- raise TypeError, ":digest must be a Digest" unless value.is_a?(Authentication::Digest)
25
+ raise TypeError, ":digest must be a #{Authentication::Digest}" unless value.is_a?(Authentication::Digest)
26
26
 
27
27
  value
28
28
  end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # https://gitlab.com/os85/httpx/wikis/OAuth
7
+ #
8
+ module OAuth
9
+ class << self
10
+ def load_dependencies(_klass)
11
+ require_relative "authentication/basic"
12
+ end
13
+ end
14
+
15
+ SUPPORTED_GRANT_TYPES = %w[client_credentials refresh_token].freeze
16
+ SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
17
+
18
+ class OAuthSession
19
+ attr_reader :token_endpoint_auth_method, :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope
20
+
21
+ def initialize(
22
+ issuer:,
23
+ client_id:,
24
+ client_secret:,
25
+ access_token: nil,
26
+ refresh_token: nil,
27
+ scope: nil,
28
+ token_endpoint: nil,
29
+ response_type: nil,
30
+ grant_type: nil,
31
+ token_endpoint_auth_method: "client_secret_basic"
32
+ )
33
+ @issuer = URI(issuer)
34
+ @client_id = client_id
35
+ @client_secret = client_secret
36
+ @token_endpoint = URI(token_endpoint) if token_endpoint
37
+ @response_type = response_type
38
+ @scope = case scope
39
+ when String
40
+ scope.split
41
+ when Array
42
+ scope
43
+ end
44
+ @access_token = access_token
45
+ @refresh_token = refresh_token
46
+ @token_endpoint_auth_method = String(token_endpoint_auth_method)
47
+ @grant_type = grant_type || (@refresh_token ? "refresh_token" : "client_credentials")
48
+
49
+ unless SUPPORTED_AUTH_METHODS.include?(@token_endpoint_auth_method)
50
+ raise Error, "#{@token_endpoint_auth_method} is not a supported auth method"
51
+ end
52
+
53
+ return if SUPPORTED_GRANT_TYPES.include?(@grant_type)
54
+
55
+ raise Error, "#{@grant_type} is not a supported grant type"
56
+ end
57
+
58
+ def token_endpoint
59
+ @token_endpoint || "#{@issuer}/token"
60
+ end
61
+
62
+ def load(http)
63
+ return unless @token_endpoint && @token_endpoint_auth_method && @grant_type && @scope
64
+
65
+ metadata = http.get("#{issuer}/.well-known/oauth-authorization-server").raise_for_status.json
66
+
67
+ @token_endpoint = metadata["token_endpoint"]
68
+ @scope = metadata["scopes_supported"]
69
+ @grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
70
+ @token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
71
+ SUPPORTED_AUTH_METHODS.include?(am)
72
+ end
73
+ end
74
+
75
+ def merge(other)
76
+ obj = dup
77
+
78
+ case other
79
+ when OAuthSession
80
+ other.instance_variables.each do |ivar|
81
+ val = other.instance_variable_get(ivar)
82
+ next unless val
83
+
84
+ obj.instance_variable_set(ivar, val)
85
+ end
86
+ when Hash
87
+ other.each do |k, v|
88
+ obj.instance_variable_set(:"@#{k}", v) if obj.instance_variable_defined?(:"@#{k}")
89
+ end
90
+ end
91
+ obj
92
+ end
93
+ end
94
+
95
+ module OptionsMethods
96
+ def option_oauth_session(value)
97
+ case value
98
+ when Hash
99
+ OAuthSession.new(**value)
100
+ when OAuthSession
101
+ value
102
+ else
103
+ raise TypeError, ":oauth_session must be a #{OAuthSession}"
104
+ end
105
+ end
106
+ end
107
+
108
+ module InstanceMethods
109
+ def oauth_authentication(**args)
110
+ with(oauth_session: OAuthSession.new(**args))
111
+ end
112
+
113
+ def with_access_token
114
+ oauth_session = @options.oauth_session
115
+
116
+ oauth_session.load(self)
117
+
118
+ grant_type = oauth_session.grant_type
119
+
120
+ headers = {}
121
+ form_post = { "grant_type" => grant_type, "scope" => Array(oauth_session.scope).join(" ") }.compact
122
+
123
+ # auth
124
+ case oauth_session.token_endpoint_auth_method
125
+ when "client_secret_basic"
126
+ headers["authorization"] = Authentication::Basic.new(oauth_session.client_id, oauth_session.client_secret).authenticate
127
+ when "client_secret_post"
128
+ form_post["client_id"] = oauth_session.client_id
129
+ form_post["client_secret"] = oauth_session.client_secret
130
+ end
131
+
132
+ case grant_type
133
+ when "client_credentials"
134
+ # do nothing
135
+ when "refresh_token"
136
+ form_post["refresh_token"] = oauth_session.refresh_token
137
+ end
138
+
139
+ token_request = build_request("POST", oauth_session.token_endpoint, headers: headers, form: form_post)
140
+ token_request.headers.delete("authorization") unless oauth_session.token_endpoint_auth_method == "client_secret_basic"
141
+
142
+ token_response = request(token_request)
143
+ token_response.raise_for_status
144
+
145
+ payload = token_response.json
146
+
147
+ access_token = payload["access_token"]
148
+ refresh_token = payload["refresh_token"]
149
+
150
+ with(oauth_session: oauth_session.merge(access_token: access_token, refresh_token: refresh_token))
151
+ end
152
+
153
+ def build_request(*, _)
154
+ request = super
155
+
156
+ return request if request.headers.key?("authorization")
157
+
158
+ oauth_session = @options.oauth_session
159
+
160
+ return request unless oauth_session && oauth_session.access_token
161
+
162
+ request.headers["authorization"] = "Bearer #{oauth_session.access_token}"
163
+
164
+ request
165
+ end
166
+ end
167
+ end
168
+ register_plugin :oauth, OAuth
169
+ end
170
+ end
@@ -6,6 +6,12 @@ module HTTPX
6
6
  module Plugins
7
7
  module Proxy
8
8
  module HTTP
9
+ class << self
10
+ def extra_options(options)
11
+ options.merge(supported_proxy_protocols: options.supported_proxy_protocols + %w[http])
12
+ end
13
+ end
14
+
9
15
  module InstanceMethods
10
16
  def with_proxy_basic_auth(opts)
11
17
  with(proxy: opts.merge(scheme: "basic"))
@@ -16,6 +16,12 @@ module HTTPX
16
16
 
17
17
  Error = Socks4Error
18
18
 
19
+ class << self
20
+ def extra_options(options)
21
+ options.merge(supported_proxy_protocols: options.supported_proxy_protocols + PROTOCOLS)
22
+ end
23
+ end
24
+
19
25
  module ConnectionMethods
20
26
  def interests
21
27
  if @state == :connecting
@@ -18,8 +18,14 @@ module HTTPX
18
18
 
19
19
  Error = Socks5Error
20
20
 
21
- def self.load_dependencies(*)
22
- require_relative "../authentication/socks5"
21
+ class << self
22
+ def load_dependencies(*)
23
+ require_relative "../authentication/socks5"
24
+ end
25
+
26
+ def extra_options(options)
27
+ options.merge(supported_proxy_protocols: options.supported_proxy_protocols + %w[socks5])
28
+ end
23
29
  end
24
30
 
25
31
  module ConnectionMethods
@@ -25,6 +25,10 @@ module HTTPX
25
25
  klass.plugin(:"proxy/socks5")
26
26
  end
27
27
 
28
+ def extra_options(options)
29
+ options.merge(supported_proxy_protocols: [])
30
+ end
31
+
28
32
  if URI::Generic.methods.include?(:use_proxy?)
29
33
  def use_proxy?(*args)
30
34
  URI::Generic.use_proxy?(*args)
@@ -118,6 +122,12 @@ module HTTPX
118
122
  def option_proxy(value)
119
123
  value.is_a?(Parameters) ? value : Hash[value]
120
124
  end
125
+
126
+ def option_supported_proxy_protocols(value)
127
+ raise TypeError, ":supported_proxy_protocols must be an Array" unless value.is_a?(Array)
128
+
129
+ value.map(&:to_s)
130
+ end
121
131
  end
122
132
 
123
133
  module InstanceMethods
@@ -142,8 +152,12 @@ module HTTPX
142
152
  next_proxy = @_proxy_uris.first
143
153
  raise Error, "Failed to connect to proxy" unless next_proxy
144
154
 
155
+ next_proxy = URI(next_proxy)
156
+
157
+ raise Error,
158
+ "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
159
+
145
160
  if proxy.key?(:no_proxy)
146
- next_proxy = URI(next_proxy)
147
161
 
148
162
  no_proxy = proxy[:no_proxy]
149
163
  no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
@@ -170,11 +184,7 @@ module HTTPX
170
184
  proxy = options.proxy
171
185
  return super unless proxy
172
186
 
173
- connection = options.connection_class.new("tcp", uri, options)
174
- catch(:coalesced) do
175
- pool.init_connection(connection, options)
176
- connection
177
- end
187
+ init_connection("tcp", uri, options)
178
188
  end
179
189
 
180
190
  def fetch_response(request, connections, options)
@@ -257,10 +267,11 @@ module HTTPX
257
267
  end
258
268
 
259
269
  def send(request)
260
- return super unless @options.proxy
261
- return super unless connecting?
270
+ return super unless (
271
+ @options.proxy && @state != :idle && connecting?
272
+ )
262
273
 
263
- @pending << request
274
+ (@proxy_pending ||= []) << request
264
275
  end
265
276
 
266
277
  def connecting?
@@ -298,6 +309,12 @@ module HTTPX
298
309
  when :idle
299
310
  transition(:connecting)
300
311
  when :connected
312
+ if @proxy_pending
313
+ while (req = @proxy_pendind.shift)
314
+ send(req)
315
+ end
316
+ end
317
+
301
318
  transition(:open)
302
319
  end
303
320
  end
@@ -1,34 +1,39 @@
1
- # # frozen_string_literal: true
2
-
3
- # require "mutex_m"
4
- # require "forwardable"
5
-
6
- # module HTTPX::Plugins
7
- # module ResponseCache
8
- # class FileStore < Store
9
- # def initialize(dir = Dir.tmpdir)
10
- # @dir = Pathname.new(dir)
11
- # @store = {}
12
- # @hash_store.extend(Mutex_m)
13
- # end
14
-
15
- # def clear
16
- # @store.synchronize { @store.clear }
17
- # end
18
-
19
- # def _get(request)
20
- # return unless File.exist?(@dir.join(request.response_cache_key))
21
-
22
- # @store.synchronize { @store[key] }
23
- # end
24
-
25
- # def set(key)
26
- # @store.synchronize do
27
- # resps = (@store[key] ||= [])
28
-
29
- # yield resps
30
- # end
31
- # end
32
- # end
33
- # end
34
- # end
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module HTTPX::Plugins
6
+ module ResponseCache
7
+ class FileStore < Store
8
+ def initialize(dir = Dir.tmpdir)
9
+ @dir = Pathname.new(dir)
10
+ end
11
+
12
+ def clear
13
+ # delete all files
14
+ end
15
+
16
+ def cached?(request)
17
+ file_path = @dir.join(request.response_cache_key)
18
+
19
+ exist?(file_path)
20
+ end
21
+
22
+ private
23
+
24
+ def _get(request)
25
+ return unless cached?(request)
26
+
27
+ File.open(@dir.join(request.response_cache_key))
28
+ end
29
+
30
+ def _set(request, response)
31
+ file_path = @dir.join(request.response_cache_key)
32
+
33
+ response.copy_to(file_path)
34
+
35
+ response.body.rewind
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,28 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "mutex_m"
4
4
 
5
5
  module HTTPX::Plugins
6
6
  module ResponseCache
7
7
  class Store
8
- extend Forwardable
9
-
10
- def_delegator :@store, :clear
11
-
12
8
  def initialize
13
9
  @store = {}
10
+ @store.extend(Mutex_m)
11
+ end
12
+
13
+ def clear
14
+ @store.synchronize { @store.clear }
14
15
  end
15
16
 
16
17
  def lookup(request)
17
- responses = @store[request.response_cache_key]
18
+ responses = _get(request)
18
19
 
19
20
  return unless responses
20
21
 
21
- response = responses.find(&method(:match_by_vary?).curry(2)[request])
22
-
23
- return unless response && response.fresh?
24
-
25
- response
22
+ responses.find(&method(:match_by_vary?).curry(2)[request])
26
23
  end
27
24
 
28
25
  def cached?(request)
@@ -32,11 +29,7 @@ module HTTPX::Plugins
32
29
  def cache(request, response)
33
30
  return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
34
31
 
35
- responses = (@store[request.response_cache_key] ||= [])
36
-
37
- responses.reject!(&method(:match_by_vary?).curry(2)[request])
38
-
39
- responses << response
32
+ _set(request, response)
40
33
  end
41
34
 
42
35
  def prepare(request)
@@ -71,6 +64,32 @@ module HTTPX::Plugins
71
64
  !original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
72
65
  end
73
66
  end
67
+
68
+ def _get(request)
69
+ @store.synchronize do
70
+ responses = @store[request.response_cache_key]
71
+
72
+ return unless responses
73
+
74
+ responses.select! do |res|
75
+ !res.body.closed? && res.fresh?
76
+ end
77
+
78
+ responses
79
+ end
80
+ end
81
+
82
+ def _set(request, response)
83
+ @store.synchronize do
84
+ responses = (@store[request.response_cache_key] ||= [])
85
+
86
+ responses.reject! do |res|
87
+ res.body.closed? || !res.fresh? || match_by_vary?(request, res)
88
+ end
89
+
90
+ responses << response
91
+ end
92
+ end
74
93
  end
75
94
  end
76
95
  end
@@ -102,9 +102,9 @@ module HTTPX
102
102
 
103
103
  module ResponseMethods
104
104
  def copy_from_cached(other)
105
- @body = other.body
105
+ @body = other.body.dup
106
106
 
107
- @body.__send__(:rewind)
107
+ @body.rewind
108
108
  end
109
109
 
110
110
  # A response is fresh if its age has not yet exceeded its freshness lifetime.
@@ -169,7 +169,7 @@ module HTTPX
169
169
  def date
170
170
  @date ||= Time.httpdate(@headers["date"])
171
171
  rescue NoMethodError, ArgumentError
172
- Time.now.httpdate
172
+ Time.now
173
173
  end
174
174
  end
175
175
  end
data/lib/httpx/request.rb CHANGED
@@ -95,6 +95,8 @@ module HTTPX
95
95
  return
96
96
  end
97
97
  @response = response
98
+
99
+ emit(:response_started, response)
98
100
  end
99
101
 
100
102
  def path
@@ -130,8 +132,10 @@ module HTTPX
130
132
  return nil if @body.nil?
131
133
 
132
134
  @drainer ||= @body.each
133
- chunk = @drainer.next
134
- chunk.dup
135
+ chunk = @drainer.next.dup
136
+
137
+ emit(:body_chunk, chunk)
138
+ chunk
135
139
  rescue StopIteration
136
140
  nil
137
141
  rescue StandardError => e
@@ -53,7 +53,7 @@ module HTTPX
53
53
 
54
54
  next if !addrs || addrs.empty?
55
55
 
56
- resolver.emit_addresses(connection, resolver.family, addrs)
56
+ resolver.emit_addresses(connection, resolver.family, addrs, true)
57
57
  end
58
58
  end
59
59
 
@@ -46,13 +46,13 @@ module HTTPX
46
46
  true
47
47
  end
48
48
 
49
- def emit_addresses(connection, family, addresses)
49
+ def emit_addresses(connection, family, addresses, early_resolve = false)
50
50
  addresses.map! do |address|
51
51
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
52
52
  end
53
53
 
54
- # double emission check
55
- return if connection.addresses && !addresses.intersect?(connection.addresses)
54
+ # double emission check, but allow early resolution to work
55
+ return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
56
56
 
57
57
  log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
58
58
  if @pool && # if triggered by early resolve, pool may not be here yet
@@ -87,7 +87,7 @@ module HTTPX
87
87
 
88
88
  return if addresses.empty?
89
89
 
90
- emit_addresses(connection, @family, addresses)
90
+ emit_addresses(connection, @family, addresses, true)
91
91
  end
92
92
 
93
93
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -9,6 +9,7 @@ require "forwardable"
9
9
  module HTTPX
10
10
  class Response
11
11
  extend Forwardable
12
+ include Callbacks
12
13
 
13
14
  attr_reader :status, :headers, :body, :version
14
15
 
@@ -139,6 +140,12 @@ module HTTPX
139
140
  @state = :idle
140
141
  end
141
142
 
143
+ def initialize_dup(other)
144
+ super
145
+
146
+ @buffer = other.instance_variable_get(:@buffer).dup
147
+ end
148
+
142
149
  def closed?
143
150
  @state == :closed
144
151
  end
@@ -146,9 +153,13 @@ module HTTPX
146
153
  def write(chunk)
147
154
  return if @state == :closed
148
155
 
149
- @length += chunk.bytesize
156
+ size = chunk.bytesize
157
+ @length += size
150
158
  transition
151
159
  @buffer.write(chunk)
160
+
161
+ @response.emit(:chunk_received, chunk)
162
+ size
152
163
  end
153
164
 
154
165
  def read(*args)
data/lib/httpx/session.rb CHANGED
@@ -4,6 +4,7 @@ module HTTPX
4
4
  class Session
5
5
  include Loggable
6
6
  include Chainable
7
+ include Callbacks
7
8
 
8
9
  EMPTY_HASH = {}.freeze
9
10
 
@@ -45,6 +46,31 @@ module HTTPX
45
46
  request = rklass.new(verb, uri, options.merge(persistent: @persistent))
46
47
  request.on(:response, &method(:on_response).curry(2)[request])
47
48
  request.on(:promise, &method(:on_promise))
49
+
50
+ request.on(:headers) do
51
+ emit(:request_started, request)
52
+ end
53
+ request.on(:body_chunk) do |chunk|
54
+ emit(:request_body_chunk, request, chunk)
55
+ end
56
+ request.on(:done) do
57
+ emit(:request_completed, request)
58
+ end
59
+
60
+ request.on(:response_started) do |res|
61
+ if res.is_a?(Response)
62
+ emit(:response_started, request, res)
63
+ res.on(:chunk_received) do |chunk|
64
+ emit(:response_body_chunk, request, res, chunk)
65
+ end
66
+ else
67
+ emit(:request_error, request, res.error)
68
+ end
69
+ end
70
+ request.on(:response) do |res|
71
+ emit(:response_completed, request, res)
72
+ end
73
+
48
74
  request
49
75
  end
50
76
 
@@ -174,7 +200,16 @@ module HTTPX
174
200
  raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
175
201
  end
176
202
  end
203
+ init_connection(type, uri, options)
204
+ end
205
+
206
+ def init_connection(type, uri, options)
177
207
  connection = options.connection_class.new(type, uri, options)
208
+ connection.on(:open) do
209
+ emit(:connection_opened, connection.origin, connection.io.socket)
210
+ # only run close callback if it opened
211
+ connection.on(:close) { emit(:connection_closed, connection.origin, connection.io.socket) }
212
+ end
178
213
  catch(:coalesced) do
179
214
  pool.init_connection(connection, options)
180
215
  connection
@@ -252,6 +287,7 @@ module HTTPX
252
287
  super
253
288
  klass.instance_variable_set(:@default_options, @default_options)
254
289
  klass.instance_variable_set(:@plugins, @plugins.dup)
290
+ klass.instance_variable_set(:@callbacks, @callbacks.dup)
255
291
  end
256
292
 
257
293
  def plugin(pl, options = nil, &block)
@@ -9,10 +9,13 @@ module HTTPX
9
9
  # redefine the default options static var, which needs to
10
10
  # refresh options_class
11
11
  options = proxy_session.class.default_options.to_hash
12
- options.freeze
13
12
  original_verbosity = $VERBOSE
14
13
  $VERBOSE = nil
14
+ const_set(:Options, proxy_session.class.default_options.options_class)
15
+ options[:options_class] = Class.new(options[:options_class])
16
+ options.freeze
15
17
  Options.send(:const_set, :DEFAULT_OPTIONS, options)
18
+ Session.instance_variable_set(:@default_options, Options.new(options))
16
19
  $VERBOSE = original_verbosity
17
20
  end
18
21
 
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.23.4"
4
+ VERSION = "0.24.1"
5
5
  end
data/sig/callbacks.rbs CHANGED
@@ -4,9 +4,9 @@ module HTTPX
4
4
  end
5
5
 
6
6
  module Callbacks
7
- def on: (Symbol) { (*untyped) -> void } -> void
8
- def once: (Symbol) { (*untyped) -> void } -> void
9
- def only: (Symbol) { (*untyped) -> void } -> void
7
+ def on: (Symbol) { (*untyped) -> void } -> self
8
+ def once: (Symbol) { (*untyped) -> void } -> self
9
+ def only: (Symbol) { (*untyped) -> void } -> self
10
10
  def emit: (Symbol, *untyped) -> void
11
11
 
12
12
  def callbacks_for?: (Symbol) -> bool
data/sig/chainable.rbs CHANGED
@@ -34,6 +34,7 @@ module HTTPX
34
34
  | (:grpc, ?options) -> Plugins::grpcSession
35
35
  | (:response_cache, ?options) -> Plugins::sessionResponseCache
36
36
  | (:circuit_breaker, ?options) -> Plugins::sessionCircuitBreaker
37
+ | (:oauth, ?options) -> Plugins::sessionOAuth
37
38
  | (Symbol | Module, ?options) { (Class) -> void } -> Session
38
39
  | (Symbol | Module, ?options) -> Session
39
40
 
@@ -5,7 +5,7 @@ module HTTPX
5
5
  class CircuitStore
6
6
  @circuits: Hash[String, Circuit]
7
7
 
8
- def try_open: (generic_uri uri, response response) -> void
8
+ def try_open: (generic_uri uri, response response) -> response?
9
9
 
10
10
  def try_respond: (Request request) -> response?
11
11
 
@@ -30,7 +30,7 @@ module HTTPX
30
30
 
31
31
  def respond: () -> response?
32
32
 
33
- def try_open: (response) -> void
33
+ def try_open: (response) -> response?
34
34
 
35
35
  def try_close: () -> void
36
36
 
@@ -52,6 +52,10 @@ module HTTPX
52
52
 
53
53
  module InstanceMethods
54
54
  @circuit_store: CircuitStore
55
+
56
+ private
57
+
58
+ def try_circuit_open: (Request request, response response) -> response?
55
59
  end
56
60
 
57
61
  end
@@ -0,0 +1,54 @@
1
+ module HTTPX
2
+ module Plugins
3
+ #
4
+ # https://gitlab.com/os85/httpx/wikis/OAuth
5
+ #
6
+ module OAuth
7
+ def self.load_dependencies: (singleton(Session) klass) -> void
8
+
9
+ type grant_type = "client_credentials" | "refresh_token"
10
+
11
+ type token_auth_method = "client_secret_basic" | "client_secret_post"
12
+
13
+ SUPPORTED_GRANT_TYPES: ::Array[grant_type]
14
+
15
+ SUPPORTED_AUTH_METHODS: ::Array[token_auth_method]
16
+
17
+ class OAuthSession
18
+ attr_reader token_endpoint_auth_method: token_auth_method
19
+
20
+ attr_reader grant_type: grant_type
21
+
22
+ attr_reader client_id: String
23
+
24
+ attr_reader client_secret: String
25
+
26
+ attr_reader access_token: String?
27
+
28
+ attr_reader refresh_token: String?
29
+
30
+ attr_reader scope: Array[String]?
31
+
32
+ def initialize: (issuer: uri, client_id: String, client_secret: String, ?access_token: String?, ?refresh_token: String?, ?scope: (Array[String] | String)?, ?token_endpoint: String?, ?response_type: String?, ?grant_type: String?, ?token_endpoint_auth_method: ::String) -> void
33
+
34
+ def token_endpoint: () -> String
35
+
36
+ def load: (Session http) -> void
37
+
38
+ def merge: (instance | Hash[untyped, untyped] other) -> instance
39
+ end
40
+
41
+ interface _AwsSdkOptions
42
+ def oauth_session: () -> OAuthSession?
43
+ end
44
+
45
+ module InstanceMethods
46
+ def oauth_authentication: (**untyped args) -> instance
47
+
48
+ def with_access_token: () -> instance
49
+ end
50
+ end
51
+
52
+ type sessionOAuth = Session & OAuth::InstanceMethods
53
+ end
54
+ end
@@ -8,7 +8,7 @@ module HTTPX
8
8
  def self?.cached_response?: (response response) -> bool
9
9
 
10
10
  class Store
11
- @store: Hash[String, Array[Response]]
11
+ @store: Hash[String, Array[Response]] & Mutex_m
12
12
 
13
13
  def lookup: (Request request) -> Response?
14
14
 
@@ -21,6 +21,10 @@ module HTTPX
21
21
  private
22
22
 
23
23
  def match_by_vary?: (Request request, Response response) -> bool
24
+
25
+ def _get: (Request request) -> Array[Response]?
26
+
27
+ def _set: (Request request, Response response) -> void
24
28
  end
25
29
 
26
30
  module InstanceMethods
@@ -18,7 +18,7 @@ module HTTPX
18
18
 
19
19
  def empty?: () -> bool
20
20
 
21
- def emit_addresses: (Connection connection, ip_family family, Array[IPAddr]) -> void
21
+ def emit_addresses: (Connection connection, ip_family family, Array[IPAddr], ?bool early_resolve) -> void
22
22
 
23
23
  private
24
24
 
data/sig/response.rbs CHANGED
@@ -9,6 +9,7 @@ module HTTPX
9
9
 
10
10
  class Response
11
11
  extend Forwardable
12
+ include Callbacks
12
13
 
13
14
  include _Response
14
15
  include _ToS
data/sig/session.rbs CHANGED
@@ -27,9 +27,9 @@ module HTTPX
27
27
  def on_promise: (untyped, untyped) -> void
28
28
  def fetch_response: (Request request, Array[Connection] connections, untyped options) -> response?
29
29
 
30
- def find_connection: (Request, Array[Connection] connections, Options options) -> Connection
30
+ def find_connection: (Request request, Array[Connection] connections, Options options) -> Connection
31
31
 
32
- def set_connection_callbacks: (Connection, Array[Connection], Options) -> void
32
+ def set_connection_callbacks: (Connection connection, Array[Connection] connections, Options options) -> void
33
33
 
34
34
  def build_altsvc_connection: (Connection existing_connection, Array[Connection] connections, URI::Generic alt_origin, String origin, Hash[String, String] alt_params, Options options) -> Connection?
35
35
 
@@ -39,13 +39,15 @@ module HTTPX
39
39
  | (verb, _Each[[uri, options]], Options) -> Array[Request]
40
40
  | (verb, _Each[uri], options) -> Array[Request]
41
41
 
42
- def build_connection: (URI::HTTP | URI::HTTPS uri, Options options) -> Connection
42
+ def build_connection: (URI::HTTP | URI::HTTP uri, Options options) -> Connection
43
+
44
+ def init_connection: (String type, URI::HTTP | URI::HTTP uri, Options options) -> Connection
43
45
 
44
46
  def send_requests: (*Request) -> Array[response]
45
47
 
46
- def _send_requests: (Array[Request]) -> Array[Connection]
48
+ def _send_requests: (Array[Request] requests) -> Array[Connection]
47
49
 
48
- def receive_requests: (Array[Request], Array[Connection]) -> Array[response]
50
+ def receive_requests: (Array[Request] requests, Array[Connection] connections) -> Array[response]
49
51
 
50
52
  attr_reader self.default_options: Options
51
53
  end
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.23.4
4
+ version: 0.24.1
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-06-08 00:00:00.000000000 Z
11
+ date: 2023-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2-next
@@ -99,6 +99,8 @@ extra_rdoc_files:
99
99
  - doc/release_notes/0_23_2.md
100
100
  - doc/release_notes/0_23_3.md
101
101
  - doc/release_notes/0_23_4.md
102
+ - doc/release_notes/0_24_0.md
103
+ - doc/release_notes/0_24_1.md
102
104
  - doc/release_notes/0_2_0.md
103
105
  - doc/release_notes/0_2_1.md
104
106
  - doc/release_notes/0_3_0.md
@@ -190,6 +192,8 @@ files:
190
192
  - doc/release_notes/0_23_2.md
191
193
  - doc/release_notes/0_23_3.md
192
194
  - doc/release_notes/0_23_4.md
195
+ - doc/release_notes/0_24_0.md
196
+ - doc/release_notes/0_24_1.md
193
197
  - doc/release_notes/0_2_0.md
194
198
  - doc/release_notes/0_2_1.md
195
199
  - doc/release_notes/0_3_0.md
@@ -268,6 +272,7 @@ files:
268
272
  - lib/httpx/plugins/multipart/mime_type_detector.rb
269
273
  - lib/httpx/plugins/multipart/part.rb
270
274
  - lib/httpx/plugins/ntlm_authentication.rb
275
+ - lib/httpx/plugins/oauth.rb
271
276
  - lib/httpx/plugins/persistent.rb
272
277
  - lib/httpx/plugins/proxy.rb
273
278
  - lib/httpx/plugins/proxy/http.rb
@@ -349,6 +354,7 @@ files:
349
354
  - sig/plugins/h2c.rbs
350
355
  - sig/plugins/multipart.rbs
351
356
  - sig/plugins/ntlm_authentication.rbs
357
+ - sig/plugins/oauth.rbs
352
358
  - sig/plugins/persistent.rbs
353
359
  - sig/plugins/proxy.rbs
354
360
  - sig/plugins/proxy/http.rbs
@@ -388,7 +394,7 @@ metadata:
388
394
  changelog_uri: https://os85.gitlab.io/httpx/#release-notes
389
395
  documentation_uri: https://os85.gitlab.io/httpx/rdoc/
390
396
  source_code_uri: https://gitlab.com/os85/httpx
391
- homepage_uri: https://os85.gitlab.io/httpx/
397
+ homepage_uri: https://honeyryderchuck.gitlab.io/httpx/
392
398
  rubygems_mfa_required: 'true'
393
399
  post_install_message:
394
400
  rdoc_options: []
@@ -405,7 +411,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
405
411
  - !ruby/object:Gem::Version
406
412
  version: '0'
407
413
  requirements: []
408
- rubygems_version: 3.4.6
414
+ rubygems_version: 3.4.10
409
415
  signing_key:
410
416
  specification_version: 4
411
417
  summary: HTTPX, to the future, and beyond