httpx 0.23.3 → 0.24.0

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: 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