httpx 0.17.0 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/doc/release_notes/0_18_0.md +69 -0
- data/doc/release_notes/0_18_1.md +12 -0
- data/doc/release_notes/0_18_2.md +10 -0
- data/doc/release_notes/0_18_3.md +7 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +5 -3
- data/lib/httpx/adapters/webmock.rb +7 -1
- data/lib/httpx/altsvc.rb +2 -2
- data/lib/httpx/chainable.rb +3 -3
- data/lib/httpx/connection/http1.rb +8 -5
- data/lib/httpx/connection/http2.rb +22 -7
- data/lib/httpx/connection.rb +70 -71
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/extensions.rb +50 -4
- data/lib/httpx/io/ssl.rb +5 -1
- data/lib/httpx/io/tls.rb +7 -7
- data/lib/httpx/loggable.rb +5 -5
- data/lib/httpx/options.rb +7 -7
- data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
- data/lib/httpx/plugins/aws_sigv4.rb +9 -11
- data/lib/httpx/plugins/compression.rb +5 -3
- data/lib/httpx/plugins/cookies/jar.rb +1 -1
- data/lib/httpx/plugins/expect.rb +7 -3
- data/lib/httpx/plugins/grpc/message.rb +2 -2
- data/lib/httpx/plugins/grpc.rb +3 -3
- data/lib/httpx/plugins/internal_telemetry.rb +8 -8
- data/lib/httpx/plugins/multipart.rb +2 -2
- data/lib/httpx/plugins/response_cache/store.rb +55 -0
- data/lib/httpx/plugins/response_cache.rb +88 -0
- data/lib/httpx/plugins/retries.rb +36 -14
- data/lib/httpx/plugins/stream.rb +1 -1
- data/lib/httpx/pool.rb +39 -13
- data/lib/httpx/request.rb +7 -7
- data/lib/httpx/resolver/https.rb +5 -7
- data/lib/httpx/resolver/native.rb +4 -2
- data/lib/httpx/resolver/system.rb +2 -0
- data/lib/httpx/resolver.rb +2 -2
- data/lib/httpx/response.rb +23 -14
- data/lib/httpx/selector.rb +12 -17
- data/lib/httpx/session.rb +7 -2
- data/lib/httpx/session2.rb +1 -1
- data/lib/httpx/timers.rb +84 -0
- data/lib/httpx/transcoder/body.rb +2 -1
- data/lib/httpx/transcoder/form.rb +1 -1
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/utils.rb +8 -0
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +1 -0
- data/sig/chainable.rbs +1 -0
- data/sig/connection/http1.rbs +5 -0
- data/sig/connection/http2.rbs +3 -0
- data/sig/connection.rbs +12 -6
- data/sig/plugins/aws_sdk_authentication.rbs +22 -4
- data/sig/plugins/response_cache.rbs +35 -0
- data/sig/plugins/retries.rbs +3 -0
- data/sig/pool.rbs +6 -0
- data/sig/resolver/native.rbs +3 -4
- data/sig/resolver/system.rbs +2 -0
- data/sig/response.rbs +3 -2
- data/sig/timers.rbs +32 -0
- data/sig/utils.rbs +4 -0
- metadata +17 -17
data/lib/httpx/extensions.rb
CHANGED
@@ -54,6 +54,51 @@ module HTTPX
|
|
54
54
|
Numeric.__send__(:include, NegMethods)
|
55
55
|
end
|
56
56
|
|
57
|
+
module HashExtensions
|
58
|
+
refine Hash do
|
59
|
+
def compact
|
60
|
+
h = {}
|
61
|
+
each do |key, value|
|
62
|
+
h[key] = value unless value == nil
|
63
|
+
end
|
64
|
+
h
|
65
|
+
end unless Hash.method_defined?(:compact)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module ArrayExtensions
|
70
|
+
refine Array do
|
71
|
+
|
72
|
+
def filter_map
|
73
|
+
return to_enum(:filter_map) unless block_given?
|
74
|
+
|
75
|
+
each_with_object([]) do |item, res|
|
76
|
+
processed = yield(item)
|
77
|
+
res << processed if processed
|
78
|
+
end
|
79
|
+
end unless Array.method_defined?(:filter_map)
|
80
|
+
|
81
|
+
def sum(accumulator = 0, &block)
|
82
|
+
values = block_given? ? map(&block) : self
|
83
|
+
values.inject(accumulator, :+)
|
84
|
+
end unless Array.method_defined?(:sum)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module IOExtensions
|
89
|
+
refine IO do
|
90
|
+
# provides a fallback for rubies where IO#wait isn't implemented,
|
91
|
+
# but IO#wait_readable and IO#wait_writable are.
|
92
|
+
def wait(timeout = nil, _mode = :read_write)
|
93
|
+
r, w = IO.select([self], [self], nil, timeout)
|
94
|
+
|
95
|
+
return unless r || w
|
96
|
+
|
97
|
+
self
|
98
|
+
end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
57
102
|
module RegexpExtensions
|
58
103
|
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
59
104
|
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
@@ -77,13 +122,14 @@ module HTTPX
|
|
77
122
|
end
|
78
123
|
|
79
124
|
def authority
|
80
|
-
|
81
|
-
|
82
|
-
|
125
|
+
return host if port == default_port
|
126
|
+
|
127
|
+
"#{host}:#{port}"
|
128
|
+
end unless URI::HTTP.method_defined?(:authority)
|
83
129
|
|
84
130
|
def origin
|
85
131
|
"#{scheme}://#{authority}"
|
86
|
-
end
|
132
|
+
end unless URI::HTTP.method_defined?(:origin)
|
87
133
|
|
88
134
|
def altsvc_match?(uri)
|
89
135
|
uri = URI.parse(uri)
|
data/lib/httpx/io/ssl.rb
CHANGED
@@ -27,6 +27,10 @@ module HTTPX
|
|
27
27
|
super
|
28
28
|
end
|
29
29
|
|
30
|
+
def can_verify_peer?
|
31
|
+
@ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
32
|
+
end
|
33
|
+
|
30
34
|
def verify_hostname(host)
|
31
35
|
return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
|
32
36
|
return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
|
@@ -134,7 +138,7 @@ module HTTPX
|
|
134
138
|
server_cert = @io.peer_cert
|
135
139
|
|
136
140
|
"#{super}\n\n" \
|
137
|
-
|
141
|
+
"SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
|
138
142
|
"ALPN, server accepted to use #{protocol}\n" \
|
139
143
|
"Server certificate:\n" \
|
140
144
|
" subject: #{server_cert.subject}\n" \
|
data/lib/httpx/io/tls.rb
CHANGED
@@ -194,15 +194,15 @@ module HTTPX
|
|
194
194
|
server_cert = @peer_cert
|
195
195
|
|
196
196
|
"#{super}\n\n" \
|
197
|
-
|
198
|
-
|
197
|
+
"SSL connection using #{@ctx.ssl_version} / #{Array(@ctx.cipher).first}\n" \
|
198
|
+
"ALPN, server accepted to use #{protocol}\n" +
|
199
199
|
(if server_cert
|
200
200
|
"Server certificate:\n" \
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
201
|
+
" subject: #{server_cert.subject}\n" \
|
202
|
+
" start date: #{server_cert.not_before}\n" \
|
203
|
+
" expire date: #{server_cert.not_after}\n" \
|
204
|
+
" issuer: #{server_cert.issuer}\n" \
|
205
|
+
" SSL certificate verify ok."
|
206
206
|
else
|
207
207
|
"SSL certificate verify failed."
|
208
208
|
end
|
data/lib/httpx/loggable.rb
CHANGED
@@ -24,15 +24,13 @@ module HTTPX
|
|
24
24
|
debug_stream << message
|
25
25
|
end
|
26
26
|
|
27
|
-
if
|
27
|
+
if Exception.instance_methods.include?(:full_message)
|
28
28
|
|
29
29
|
def log_exception(ex, level: @options.debug_level, color: nil)
|
30
30
|
return unless @options.debug
|
31
31
|
return unless @options.debug_level >= level
|
32
32
|
|
33
|
-
|
34
|
-
message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
|
35
|
-
log(level: level, color: color) { message }
|
33
|
+
log(level: level, color: color) { ex.full_message }
|
36
34
|
end
|
37
35
|
|
38
36
|
else
|
@@ -41,7 +39,9 @@ module HTTPX
|
|
41
39
|
return unless @options.debug
|
42
40
|
return unless @options.debug_level >= level
|
43
41
|
|
44
|
-
|
42
|
+
message = +"#{ex.message} (#{ex.class})"
|
43
|
+
message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
|
44
|
+
log(level: level, color: color) { message }
|
45
45
|
end
|
46
46
|
|
47
47
|
end
|
data/lib/httpx/options.rb
CHANGED
@@ -81,9 +81,9 @@ module HTTPX
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def def_option(optname, *args, &block)
|
84
|
-
if args.size.zero? && !
|
84
|
+
if args.size.zero? && !block
|
85
85
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
86
|
-
def option_#{optname}(v); v; end
|
86
|
+
def option_#{optname}(v); v; end # def option_smth(v); v; end
|
87
87
|
OUT
|
88
88
|
return
|
89
89
|
end
|
@@ -93,15 +93,15 @@ module HTTPX
|
|
93
93
|
|
94
94
|
def deprecated_def_option(optname, layout = nil, &interpreter)
|
95
95
|
warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
|
96
|
-
|
96
|
+
"Define module OptionsMethods and `def option_#{optname}(val)` instead."
|
97
97
|
|
98
98
|
if layout
|
99
99
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
100
|
-
def option_#{optname}(value)
|
101
|
-
#{layout}
|
102
|
-
end
|
100
|
+
def option_#{optname}(value) # def option_origin(v)
|
101
|
+
#{layout} # URI(v)
|
102
|
+
end # end
|
103
103
|
OUT
|
104
|
-
elsif
|
104
|
+
elsif interpreter
|
105
105
|
define_method(:"option_#{optname}") do |value|
|
106
106
|
instance_exec(value, &interpreter)
|
107
107
|
end
|
@@ -8,6 +8,23 @@ module HTTPX
|
|
8
8
|
# It requires the "aws-sdk-core" gem.
|
9
9
|
#
|
10
10
|
module AwsSdkAuthentication
|
11
|
+
# Mock configuration, to be used only when resolving credentials
|
12
|
+
class Configuration
|
13
|
+
attr_reader :profile
|
14
|
+
|
15
|
+
def initialize(profile)
|
16
|
+
@profile = profile
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(*)
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(*)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
11
28
|
#
|
12
29
|
# encapsulates access to an AWS SDK credentials store.
|
13
30
|
#
|
@@ -30,23 +47,8 @@ module HTTPX
|
|
30
47
|
end
|
31
48
|
|
32
49
|
class << self
|
33
|
-
attr_reader :credentials, :region
|
34
|
-
|
35
50
|
def load_dependencies(_klass)
|
36
51
|
require "aws-sdk-core"
|
37
|
-
|
38
|
-
client = Class.new(Seahorse::Client::Base) do
|
39
|
-
@identifier = :httpx
|
40
|
-
set_api(Aws::S3::ClientApi::API)
|
41
|
-
add_plugin(Aws::Plugins::CredentialsConfiguration)
|
42
|
-
add_plugin(Aws::Plugins::RegionalEndpoint)
|
43
|
-
class << self
|
44
|
-
attr_reader :identifier
|
45
|
-
end
|
46
|
-
end.new
|
47
|
-
|
48
|
-
@credentials = Credentials.new(client.config[:credentials])
|
49
|
-
@region = client.config[:region]
|
50
52
|
end
|
51
53
|
|
52
54
|
def configure(klass)
|
@@ -56,6 +58,26 @@ module HTTPX
|
|
56
58
|
def extra_options(options)
|
57
59
|
options.merge(max_concurrent_requests: 1)
|
58
60
|
end
|
61
|
+
|
62
|
+
def credentials(profile)
|
63
|
+
mock_configuration = Configuration.new(profile)
|
64
|
+
Credentials.new(Aws::CredentialProviderChain.new(mock_configuration).resolve)
|
65
|
+
end
|
66
|
+
|
67
|
+
def region(profile)
|
68
|
+
# https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L62
|
69
|
+
keys = %w[AWS_REGION AMAZON_REGION AWS_DEFAULT_REGION]
|
70
|
+
env_region = ENV.values_at(*keys).compact.first
|
71
|
+
env_region = nil if env_region == ""
|
72
|
+
cfg_region = Aws.shared_config.region(profile: profile)
|
73
|
+
env_region || cfg_region
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module OptionsMethods
|
78
|
+
def option_aws_profile(value)
|
79
|
+
String(value)
|
80
|
+
end
|
59
81
|
end
|
60
82
|
|
61
83
|
module InstanceMethods
|
@@ -64,9 +86,11 @@ module HTTPX
|
|
64
86
|
# aws_authentication(credentials: Aws::Credentials.new('akid', 'secret'))
|
65
87
|
# aws_authentication()
|
66
88
|
#
|
67
|
-
def aws_sdk_authentication(
|
68
|
-
credentials
|
69
|
-
region
|
89
|
+
def aws_sdk_authentication(
|
90
|
+
credentials: AwsSdkAuthentication.credentials(@options.aws_profile),
|
91
|
+
region: AwsSdkAuthentication.region(@options.aws_profile),
|
92
|
+
**options
|
93
|
+
)
|
70
94
|
|
71
95
|
aws_sigv4_authentication(
|
72
96
|
credentials: credentials,
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
|
-
require "aws-sdk-s3"
|
5
|
-
|
6
3
|
module HTTPX
|
7
4
|
module Plugins
|
8
5
|
#
|
@@ -75,16 +72,16 @@ module HTTPX
|
|
75
72
|
|
76
73
|
# canonical request
|
77
74
|
creq = "#{request.verb.to_s.upcase}" \
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
75
|
+
"\n#{request.canonical_path}" \
|
76
|
+
"\n#{request.canonical_query}" \
|
77
|
+
"\n#{canonical_headers}" \
|
78
|
+
"\n#{signed_headers}" \
|
79
|
+
"\n#{content_hashed}"
|
83
80
|
|
84
81
|
credential_scope = "#{date}" \
|
85
|
-
|
86
|
-
|
87
|
-
|
82
|
+
"/#{@region}" \
|
83
|
+
"/#{@service}" \
|
84
|
+
"/#{lower_provider_prefix}_request"
|
88
85
|
|
89
86
|
algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
|
90
87
|
# string to sign
|
@@ -142,6 +139,7 @@ module HTTPX
|
|
142
139
|
|
143
140
|
class << self
|
144
141
|
def load_dependencies(*)
|
142
|
+
require "set"
|
145
143
|
require "digest/sha2"
|
146
144
|
require "openssl"
|
147
145
|
end
|
@@ -72,6 +72,8 @@ module HTTPX
|
|
72
72
|
end
|
73
73
|
|
74
74
|
module ResponseBodyMethods
|
75
|
+
using ArrayExtensions
|
76
|
+
|
75
77
|
attr_reader :encodings
|
76
78
|
|
77
79
|
def initialize(*)
|
@@ -90,7 +92,7 @@ module HTTPX
|
|
90
92
|
Float::INFINITY
|
91
93
|
end
|
92
94
|
|
93
|
-
@_inflaters = @headers.get("content-encoding").
|
95
|
+
@_inflaters = @headers.get("content-encoding").filter_map do |encoding|
|
94
96
|
next if encoding == "identity"
|
95
97
|
|
96
98
|
inflater = @options.encodings.registry(encoding).inflater(compressed_length)
|
@@ -100,7 +102,7 @@ module HTTPX
|
|
100
102
|
|
101
103
|
@encodings << encoding
|
102
104
|
inflater
|
103
|
-
end
|
105
|
+
end
|
104
106
|
|
105
107
|
# this can happen if the only declared encoding is "identity"
|
106
108
|
remove_instance_variable(:@_inflaters) if @_inflaters.empty?
|
@@ -134,7 +136,7 @@ module HTTPX
|
|
134
136
|
end
|
135
137
|
|
136
138
|
def each(&blk)
|
137
|
-
return enum_for(__method__) unless
|
139
|
+
return enum_for(__method__) unless blk
|
138
140
|
|
139
141
|
return deflate(&blk) if @buffer.size.zero?
|
140
142
|
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -69,9 +69,14 @@ module HTTPX
|
|
69
69
|
end
|
70
70
|
|
71
71
|
module ConnectionMethods
|
72
|
-
def
|
72
|
+
def send_request_to_parser(request)
|
73
|
+
super
|
74
|
+
|
75
|
+
return unless request.headers["expect"] == "100-continue"
|
76
|
+
|
73
77
|
request.once(:expect) do
|
74
|
-
@timers.after(
|
78
|
+
@timers.after(request.options.expect_timeout) do
|
79
|
+
# expect timeout expired
|
75
80
|
if request.state == :expect && !request.expects?
|
76
81
|
Expect.no_expect_store << request.origin
|
77
82
|
request.headers.delete("expect")
|
@@ -79,7 +84,6 @@ module HTTPX
|
|
79
84
|
end
|
80
85
|
end
|
81
86
|
end
|
82
|
-
super
|
83
87
|
end
|
84
88
|
end
|
85
89
|
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
17
17
|
|
18
18
|
# lazy decodes a grpc stream response
|
19
19
|
def stream(response, &block)
|
20
|
-
return enum_for(__method__, response) unless
|
20
|
+
return enum_for(__method__, response) unless block
|
21
21
|
|
22
22
|
response.each do |frame|
|
23
23
|
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
@@ -57,7 +57,7 @@ module HTTPX
|
|
57
57
|
|
58
58
|
yield data
|
59
59
|
|
60
|
-
message = message.byteslice(5 + size..-1)
|
60
|
+
message = message.byteslice((5 + size)..-1)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
data/lib/httpx/plugins/grpc.rb
CHANGED
@@ -143,9 +143,9 @@ module HTTPX
|
|
143
143
|
|
144
144
|
session_class = Class.new(self.class) do
|
145
145
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
146
|
-
def #{rpc_name}(input, **opts)
|
147
|
-
rpc_execute("#{rpc_name}", input, **opts)
|
148
|
-
end
|
146
|
+
def #{rpc_name}(input, **opts) # def grpc_action(input, **opts)
|
147
|
+
rpc_execute("#{rpc_name}", input, **opts) # rpc_execute("grpc_action", input, **opts)
|
148
|
+
end # end
|
149
149
|
OUT
|
150
150
|
end
|
151
151
|
|
@@ -44,6 +44,11 @@ module HTTPX
|
|
44
44
|
meter_elapsed_time("Session: initialized!!!")
|
45
45
|
end
|
46
46
|
|
47
|
+
def close(*)
|
48
|
+
super
|
49
|
+
meter_elapsed_time("Session -> close")
|
50
|
+
end
|
51
|
+
|
47
52
|
private
|
48
53
|
|
49
54
|
def build_requests(*)
|
@@ -55,11 +60,6 @@ module HTTPX
|
|
55
60
|
meter_elapsed_time("Session -> response") if response
|
56
61
|
response
|
57
62
|
end
|
58
|
-
|
59
|
-
def close(*)
|
60
|
-
super
|
61
|
-
meter_elapsed_time("Session -> close")
|
62
|
-
end
|
63
63
|
end
|
64
64
|
|
65
65
|
module RequestMethods
|
@@ -69,9 +69,9 @@ module HTTPX
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def transition(nextstate)
|
72
|
-
|
72
|
+
prev_state = @state
|
73
73
|
super
|
74
|
-
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{
|
74
|
+
meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{prev_state}] -> #{@state}") if prev_state != @state
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -84,7 +84,7 @@ module HTTPX
|
|
84
84
|
def transition(nextstate)
|
85
85
|
state = @state
|
86
86
|
super
|
87
|
-
meter_elapsed_time("Connection[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
87
|
+
meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -29,8 +29,8 @@ module HTTPX
|
|
29
29
|
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
30
30
|
require "http/form_data"
|
31
31
|
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
32
|
-
|
33
|
-
|
32
|
+
"https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
33
|
+
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
34
34
|
end
|
35
35
|
rescue LoadError
|
36
36
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module HTTPX::Plugins
|
6
|
+
module ResponseCache
|
7
|
+
class Store
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegator :@store, :clear
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@store = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def lookup(uri)
|
17
|
+
@store[uri]
|
18
|
+
end
|
19
|
+
|
20
|
+
def cached?(uri)
|
21
|
+
@store.key?(uri)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cache(uri, response)
|
25
|
+
@store[uri] = response
|
26
|
+
end
|
27
|
+
|
28
|
+
def prepare(request)
|
29
|
+
cached_response = @store[request.uri]
|
30
|
+
|
31
|
+
return unless cached_response
|
32
|
+
|
33
|
+
original_request = cached_response.instance_variable_get(:@request)
|
34
|
+
|
35
|
+
if (vary = cached_response.headers["vary"])
|
36
|
+
if vary == "*"
|
37
|
+
return unless request.headers.same_headers?(original_request.headers)
|
38
|
+
else
|
39
|
+
return unless vary.split(/ *, */).all? do |cache_field|
|
40
|
+
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
46
|
+
request.headers.add("if-modified-since", last_modified)
|
47
|
+
end
|
48
|
+
|
49
|
+
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
|
50
|
+
request.headers.add("if-none-match", etag)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds support for retrying requests when certain errors happen.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/Response-Cache
|
9
|
+
#
|
10
|
+
module ResponseCache
|
11
|
+
CACHEABLE_VERBS = %i[get head].freeze
|
12
|
+
private_constant :CACHEABLE_VERBS
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def load_dependencies(*)
|
16
|
+
require_relative "response_cache/store"
|
17
|
+
end
|
18
|
+
|
19
|
+
def cacheable_request?(request)
|
20
|
+
CACHEABLE_VERBS.include?(request.verb)
|
21
|
+
end
|
22
|
+
|
23
|
+
def cacheable_response?(response)
|
24
|
+
response.is_a?(Response) &&
|
25
|
+
# partial responses shall not be cached, only full ones.
|
26
|
+
response.status != 206 && (
|
27
|
+
response.headers.key?("etag") || response.headers.key?("last-modified-at")
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def cached_response?(response)
|
32
|
+
response.is_a?(Response) && response.status == 304
|
33
|
+
end
|
34
|
+
|
35
|
+
def extra_options(options)
|
36
|
+
options.merge(response_cache_store: Store.new)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module OptionsMethods
|
41
|
+
def option_response_cache_store(value)
|
42
|
+
raise TypeError, "must be an instance of #{Store}" unless value.is_a?(Store)
|
43
|
+
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module InstanceMethods
|
49
|
+
def clear_response_cache
|
50
|
+
@options.response_cache_store.clear
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_request(*)
|
54
|
+
request = super
|
55
|
+
return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request.uri)
|
56
|
+
|
57
|
+
@options.response_cache_store.prepare(request)
|
58
|
+
|
59
|
+
request
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch_response(request, *)
|
63
|
+
response = super
|
64
|
+
|
65
|
+
if response && ResponseCache.cached_response?(response)
|
66
|
+
log { "returning cached response for #{request.uri}" }
|
67
|
+
cached_response = @options.response_cache_store.lookup(request.uri)
|
68
|
+
|
69
|
+
response.copy_from_cached(cached_response)
|
70
|
+
end
|
71
|
+
|
72
|
+
@options.response_cache_store.cache(request.uri, response) if response && ResponseCache.cacheable_response?(response)
|
73
|
+
|
74
|
+
response
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module ResponseMethods
|
79
|
+
def copy_from_cached(other)
|
80
|
+
@body = other.body
|
81
|
+
|
82
|
+
@body.__send__(:rewind)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
register_plugin :response_cache, ResponseCache
|
87
|
+
end
|
88
|
+
end
|