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 +4 -4
- data/doc/release_notes/0_23_4.md +5 -0
- data/doc/release_notes/0_24_0.md +48 -0
- data/lib/httpx/callbacks.rb +2 -0
- data/lib/httpx/chainable.rb +19 -0
- data/lib/httpx/connection/http1.rb +21 -8
- data/lib/httpx/connection/http2.rb +1 -0
- data/lib/httpx/io/tcp.rb +4 -1
- data/lib/httpx/plugins/authentication/digest.rb +2 -3
- data/lib/httpx/plugins/circuit_breaker.rb +16 -2
- data/lib/httpx/plugins/digest_authentication.rb +1 -1
- data/lib/httpx/plugins/oauth.rb +170 -0
- data/lib/httpx/plugins/proxy.rb +1 -5
- data/lib/httpx/plugins/response_cache/file_store.rb +39 -0
- data/lib/httpx/plugins/response_cache/store.rb +35 -16
- data/lib/httpx/plugins/response_cache.rb +3 -3
- data/lib/httpx/request.rb +6 -2
- data/lib/httpx/resolver/multi.rb +1 -1
- data/lib/httpx/resolver/resolver.rb +4 -4
- data/lib/httpx/response.rb +24 -5
- data/lib/httpx/session.rb +36 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +1 -0
- data/sig/plugins/circuit_breaker.rbs +6 -2
- data/sig/plugins/oauth.rbs +54 -0
- data/sig/plugins/response_cache.rbs +5 -1
- data/sig/resolver/resolver.rbs +1 -1
- data/sig/response.rbs +5 -2
- data/sig/session.rbs +7 -5
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 159ab63d2464f90d5b73651241f85443c6b79d5d4e0e542cbb93a8e876fd9c97
|
4
|
+
data.tar.gz: 43c759345b7c52114bea8066ce7e27dd1b026a85bbdf624d1d81f12ce3314248
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4139bbc4d97e28c7c12dcaa9c3a1b71490c9b870cb60d432fb07c946af6b9346f71c8b7554abd30f584fe67b790c8095fa658571769c6a1f527b451604327354
|
7
|
+
data.tar.gz: 07b636c4eaf69fe3e60c10f4f86a0f8a58e9b1b431d27a9bf7a8b431fba2bdec8f1d4175b5b25de4487d02e90c38b00eaa5b789aacd17b5d3ced34790da55948
|
@@ -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.
|
data/lib/httpx/callbacks.rb
CHANGED
data/lib/httpx/chainable.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
161
|
+
request = @request
|
162
|
+
|
163
|
+
if request.expects?
|
154
164
|
@parser.reset!
|
155
|
-
return handle(
|
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
|
-
|
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
|
data/lib/httpx/io/tcp.rb
CHANGED
@@ -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"]}
|
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
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -170,11 +170,7 @@ module HTTPX
|
|
170
170
|
proxy = options.proxy
|
171
171
|
return super unless proxy
|
172
172
|
|
173
|
-
|
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 "
|
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 =
|
18
|
+
responses = _get(request)
|
18
19
|
|
19
20
|
return unless responses
|
20
21
|
|
21
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
data/lib/httpx/resolver/multi.rb
CHANGED
@@ -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)
|
data/lib/httpx/response.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
168
|
+
unless @reader
|
169
|
+
rewind
|
170
|
+
@reader = @buffer
|
171
|
+
end
|
156
172
|
|
157
|
-
@
|
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
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 } ->
|
8
|
-
def once: (Symbol) { (*untyped) -> void } ->
|
9
|
-
def only: (Symbol) { (*untyped) -> 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) ->
|
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) ->
|
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
|
data/sig/resolver/resolver.rbs
CHANGED
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) ->
|
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::
|
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.
|
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-
|
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
|