httpx 0.23.3 → 0.24.0

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: 9a826e525e4ea1e2836566d70fa8af5231580bc0836482a86b4af8a159785895
4
- data.tar.gz: 13d7e254551f8b730361f347b9570e4af4544c63d98ef1ca9a25eea51e10b340
3
+ metadata.gz: 159ab63d2464f90d5b73651241f85443c6b79d5d4e0e542cbb93a8e876fd9c97
4
+ data.tar.gz: 43c759345b7c52114bea8066ce7e27dd1b026a85bbdf624d1d81f12ce3314248
5
5
  SHA512:
6
- metadata.gz: e1db33022d9e917911a570cd4773078dfe5feb4198fc58e65da04cfcc278fb8569af4b8754b60b7545897546f8da05f62b56557c132f02dfcfc628923d605f47
7
- data.tar.gz: bffcd04f84fd74c825d186215d919b943349c8cd7672bf030c81696956f5625216e4118e8a16f6cdea8321f62d8fc018fbd17ca8dbeb2ad667ca847aca46de44
6
+ metadata.gz: 4139bbc4d97e28c7c12dcaa9c3a1b71490c9b870cb60d432fb07c946af6b9346f71c8b7554abd30f584fe67b790c8095fa658571769c6a1f527b451604327354
7
+ data.tar.gz: 07b636c4eaf69fe3e60c10f4f86a0f8a58e9b1b431d27a9bf7a8b431fba2bdec8f1d4175b5b25de4487d02e90c38b00eaa5b789aacd17b5d3ced34790da55948
@@ -0,0 +1,5 @@
1
+ # 0.23.4
2
+
3
+ ## Bugfixes
4
+
5
+ * fix `Response::Body#read` which rewinds on every call.
@@ -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.
@@ -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
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
@@ -170,11 +170,7 @@ module HTTPX
170
170
  proxy = options.proxy
171
171
  return super unless proxy
172
172
 
173
- connection = options.connection_class.new("tcp", uri, options)
174
- catch(:coalesced) do
175
- pool.init_connection(connection, options)
176
- connection
177
- end
173
+ init_connection("tcp", uri, options)
178
174
  end
179
175
 
180
176
  def fetch_response(request, connections, options)
@@ -0,0 +1,39 @@
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
 
@@ -107,6 +108,8 @@ module HTTPX
107
108
 
108
109
  raise Error, "no decoder available for \"#{transcoder}\"" unless decoder
109
110
 
111
+ @body.rewind
112
+
110
113
  decoder.call(self, *args)
111
114
  end
112
115
 
@@ -137,6 +140,12 @@ module HTTPX
137
140
  @state = :idle
138
141
  end
139
142
 
143
+ def initialize_dup(other)
144
+ super
145
+
146
+ @buffer = other.instance_variable_get(:@buffer).dup
147
+ end
148
+
140
149
  def closed?
141
150
  @state == :closed
142
151
  end
@@ -144,17 +153,24 @@ module HTTPX
144
153
  def write(chunk)
145
154
  return if @state == :closed
146
155
 
147
- @length += chunk.bytesize
156
+ size = chunk.bytesize
157
+ @length += size
148
158
  transition
149
159
  @buffer.write(chunk)
160
+
161
+ @response.emit(:chunk_received, chunk)
162
+ size
150
163
  end
151
164
 
152
165
  def read(*args)
153
166
  return unless @buffer
154
167
 
155
- rewind
168
+ unless @reader
169
+ rewind
170
+ @reader = @buffer
171
+ end
156
172
 
157
- @buffer.read(*args)
173
+ @reader.read(*args)
158
174
  end
159
175
 
160
176
  def bytesize
@@ -249,14 +265,17 @@ module HTTPX
249
265
  end
250
266
  # :nocov:
251
267
 
252
- private
253
-
254
268
  def rewind
255
269
  return unless @buffer
256
270
 
271
+ # in case there's some reading going on
272
+ @reader = nil
273
+
257
274
  @buffer.rewind
258
275
  end
259
276
 
277
+ private
278
+
260
279
  def transition
261
280
  case @state
262
281
  when :idle
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)
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.3"
4
+ VERSION = "0.24.0"
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
@@ -58,6 +59,7 @@ module HTTPX
58
59
  @window_size: Integer
59
60
  @length: Integer
60
61
  @buffer: StringIO | Tempfile | nil
62
+ @reader: StringIO | Tempfile | nil
61
63
 
62
64
  def write:(String chunk) -> Integer?
63
65
 
@@ -72,10 +74,11 @@ module HTTPX
72
74
  def close: () -> void
73
75
  def closed?: () -> bool
74
76
 
77
+ def rewind: () -> void
78
+
75
79
  private
76
80
 
77
- def initialize: (Response, Options) -> untyped
78
- def rewind: () -> void
81
+ def initialize: (Response, Options) -> void
79
82
  def transition: () -> void
80
83
  def _with_same_buffer_pos: [A] () { () -> A } -> A
81
84
  end
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.3
4
+ version: 0.24.0
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-05-23 00:00:00.000000000 Z
11
+ date: 2023-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2-next
@@ -98,6 +98,8 @@ extra_rdoc_files:
98
98
  - doc/release_notes/0_23_1.md
99
99
  - doc/release_notes/0_23_2.md
100
100
  - doc/release_notes/0_23_3.md
101
+ - doc/release_notes/0_23_4.md
102
+ - doc/release_notes/0_24_0.md
101
103
  - doc/release_notes/0_2_0.md
102
104
  - doc/release_notes/0_2_1.md
103
105
  - doc/release_notes/0_3_0.md
@@ -188,6 +190,8 @@ files:
188
190
  - doc/release_notes/0_23_1.md
189
191
  - doc/release_notes/0_23_2.md
190
192
  - doc/release_notes/0_23_3.md
193
+ - doc/release_notes/0_23_4.md
194
+ - doc/release_notes/0_24_0.md
191
195
  - doc/release_notes/0_2_0.md
192
196
  - doc/release_notes/0_2_1.md
193
197
  - doc/release_notes/0_3_0.md
@@ -266,6 +270,7 @@ files:
266
270
  - lib/httpx/plugins/multipart/mime_type_detector.rb
267
271
  - lib/httpx/plugins/multipart/part.rb
268
272
  - lib/httpx/plugins/ntlm_authentication.rb
273
+ - lib/httpx/plugins/oauth.rb
269
274
  - lib/httpx/plugins/persistent.rb
270
275
  - lib/httpx/plugins/proxy.rb
271
276
  - lib/httpx/plugins/proxy/http.rb
@@ -275,6 +280,7 @@ files:
275
280
  - lib/httpx/plugins/push_promise.rb
276
281
  - lib/httpx/plugins/rate_limiter.rb
277
282
  - lib/httpx/plugins/response_cache.rb
283
+ - lib/httpx/plugins/response_cache/file_store.rb
278
284
  - lib/httpx/plugins/response_cache/store.rb
279
285
  - lib/httpx/plugins/retries.rb
280
286
  - lib/httpx/plugins/stream.rb
@@ -346,6 +352,7 @@ files:
346
352
  - sig/plugins/h2c.rbs
347
353
  - sig/plugins/multipart.rbs
348
354
  - sig/plugins/ntlm_authentication.rbs
355
+ - sig/plugins/oauth.rbs
349
356
  - sig/plugins/persistent.rbs
350
357
  - sig/plugins/proxy.rbs
351
358
  - sig/plugins/proxy/http.rbs