http 5.3.1 → 6.0.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/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -1,42 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "zlib"
|
|
4
3
|
require "tempfile"
|
|
4
|
+
require "zlib"
|
|
5
5
|
|
|
6
6
|
require "http/request/body"
|
|
7
7
|
|
|
8
8
|
module HTTP
|
|
9
9
|
module Features
|
|
10
|
+
# Automatically compresses request bodies with gzip or deflate
|
|
10
11
|
class AutoDeflate < Feature
|
|
12
|
+
# Supported compression methods
|
|
13
|
+
VALID_METHODS = Set.new(%w[gzip deflate]).freeze
|
|
14
|
+
|
|
15
|
+
# Compression method name
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# feature.method # => "gzip"
|
|
19
|
+
#
|
|
20
|
+
# @return [String] compression method name
|
|
21
|
+
# @api public
|
|
11
22
|
attr_reader :method
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
# Initializes the AutoDeflate feature
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# AutoDeflate.new(method: "gzip")
|
|
28
|
+
#
|
|
29
|
+
# @param method [String] compression method ("gzip" or "deflate")
|
|
30
|
+
# @return [AutoDeflate]
|
|
31
|
+
# @api public
|
|
32
|
+
def initialize(method: "gzip")
|
|
33
|
+
super()
|
|
15
34
|
|
|
16
|
-
@method =
|
|
35
|
+
@method = String(method)
|
|
17
36
|
|
|
18
|
-
raise Error, "Only gzip and deflate methods are supported" unless
|
|
37
|
+
raise Error, "Only gzip and deflate methods are supported" unless VALID_METHODS.include?(@method)
|
|
19
38
|
end
|
|
20
39
|
|
|
40
|
+
# Wraps a request with compressed body
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# feature.wrap_request(request)
|
|
44
|
+
#
|
|
45
|
+
# @param request [HTTP::Request]
|
|
46
|
+
# @return [HTTP::Request]
|
|
47
|
+
# @api public
|
|
21
48
|
def wrap_request(request)
|
|
22
49
|
return request unless method
|
|
23
|
-
return request if request.body.
|
|
50
|
+
return request if request.body.empty?
|
|
24
51
|
|
|
25
52
|
# We need to delete Content-Length header. It will be set automatically by HTTP::Request::Writer
|
|
26
53
|
request.headers.delete(Headers::CONTENT_LENGTH)
|
|
27
54
|
request.headers[Headers::CONTENT_ENCODING] = method
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
:version => request.version,
|
|
31
|
-
:verb => request.verb,
|
|
32
|
-
:uri => request.uri,
|
|
33
|
-
:headers => request.headers,
|
|
34
|
-
:proxy => request.proxy,
|
|
35
|
-
:body => deflated_body(request.body),
|
|
36
|
-
:uri_normalizer => request.uri_normalizer
|
|
37
|
-
)
|
|
56
|
+
build_deflated_request(request)
|
|
38
57
|
end
|
|
39
58
|
|
|
59
|
+
# Returns a compressed body for the given body
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# feature.deflated_body(body)
|
|
63
|
+
#
|
|
64
|
+
# @param body [HTTP::Request::Body]
|
|
65
|
+
# @return [GzippedBody, DeflatedBody, nil]
|
|
66
|
+
# @api public
|
|
40
67
|
def deflated_body(body)
|
|
41
68
|
case method
|
|
42
69
|
when "gzip"
|
|
@@ -46,21 +73,62 @@ module HTTP
|
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Build a new request with deflated body
|
|
79
|
+
# @return [HTTP::Request]
|
|
80
|
+
# @api private
|
|
81
|
+
def build_deflated_request(request)
|
|
82
|
+
Request.new(
|
|
83
|
+
version: request.version,
|
|
84
|
+
verb: request.verb,
|
|
85
|
+
uri: request.uri,
|
|
86
|
+
headers: request.headers,
|
|
87
|
+
proxy: request.proxy,
|
|
88
|
+
body: deflated_body(request.body),
|
|
89
|
+
uri_normalizer: request.uri_normalizer
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
49
93
|
HTTP::Options.register_feature(:auto_deflate, self)
|
|
50
94
|
|
|
95
|
+
# Base class for compressed request body wrappers
|
|
51
96
|
class CompressedBody < HTTP::Request::Body
|
|
97
|
+
# Initializes a compressed body wrapper
|
|
98
|
+
#
|
|
99
|
+
# @example
|
|
100
|
+
# CompressedBody.new(uncompressed_body)
|
|
101
|
+
#
|
|
102
|
+
# @param uncompressed_body [HTTP::Request::Body]
|
|
103
|
+
# @return [CompressedBody]
|
|
104
|
+
# @api public
|
|
52
105
|
def initialize(uncompressed_body)
|
|
106
|
+
super(nil)
|
|
53
107
|
@body = uncompressed_body
|
|
54
108
|
@compressed = nil
|
|
55
109
|
end
|
|
56
110
|
|
|
111
|
+
# Returns the size of the compressed body
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# compressed_body.size
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer]
|
|
117
|
+
# @api public
|
|
57
118
|
def size
|
|
58
119
|
compress_all! unless @compressed
|
|
59
120
|
@compressed.size
|
|
60
121
|
end
|
|
61
122
|
|
|
123
|
+
# Yields each chunk of compressed data
|
|
124
|
+
#
|
|
125
|
+
# @example
|
|
126
|
+
# compressed_body.each { |chunk| io.write(chunk) }
|
|
127
|
+
#
|
|
128
|
+
# @return [self, Enumerator]
|
|
129
|
+
# @api public
|
|
62
130
|
def each(&block)
|
|
63
|
-
return to_enum
|
|
131
|
+
return to_enum(:each) unless block
|
|
64
132
|
|
|
65
133
|
if @compressed
|
|
66
134
|
compressed_each(&block)
|
|
@@ -73,6 +141,9 @@ module HTTP
|
|
|
73
141
|
|
|
74
142
|
private
|
|
75
143
|
|
|
144
|
+
# Yield each chunk from compressed data
|
|
145
|
+
# @return [void]
|
|
146
|
+
# @api private
|
|
76
147
|
def compressed_each
|
|
77
148
|
while (data = @compressed.read(Connection::BUFFER_SIZE))
|
|
78
149
|
yield data
|
|
@@ -81,14 +152,25 @@ module HTTP
|
|
|
81
152
|
@compressed.close!
|
|
82
153
|
end
|
|
83
154
|
|
|
155
|
+
# Compress all data to a tempfile
|
|
156
|
+
# @return [void]
|
|
157
|
+
# @api private
|
|
84
158
|
def compress_all!
|
|
85
|
-
@compressed = Tempfile.new("http-compressed_body", :
|
|
159
|
+
@compressed = Tempfile.new("http-compressed_body", binmode: true)
|
|
86
160
|
compress { |data| @compressed.write(data) }
|
|
87
161
|
@compressed.rewind
|
|
88
162
|
end
|
|
89
163
|
end
|
|
90
164
|
|
|
165
|
+
# Gzip-compressed request body wrapper
|
|
91
166
|
class GzippedBody < CompressedBody
|
|
167
|
+
# Compresses data using gzip
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# gzipped_body.compress { |data| io.write(data) }
|
|
171
|
+
#
|
|
172
|
+
# @return [nil]
|
|
173
|
+
# @api public
|
|
92
174
|
def compress(&block)
|
|
93
175
|
gzip = Zlib::GzipWriter.new(BlockIO.new(block))
|
|
94
176
|
@body.each { |chunk| gzip.write(chunk) }
|
|
@@ -96,18 +178,43 @@ module HTTP
|
|
|
96
178
|
gzip.finish
|
|
97
179
|
end
|
|
98
180
|
|
|
181
|
+
# IO adapter that delegates writes to a block
|
|
99
182
|
class BlockIO
|
|
183
|
+
# Initializes a block-based IO adapter
|
|
184
|
+
#
|
|
185
|
+
# @example
|
|
186
|
+
# BlockIO.new(block)
|
|
187
|
+
#
|
|
188
|
+
# @param block [Proc]
|
|
189
|
+
# @return [BlockIO]
|
|
190
|
+
# @api public
|
|
100
191
|
def initialize(block)
|
|
101
192
|
@block = block
|
|
102
193
|
end
|
|
103
194
|
|
|
195
|
+
# Writes data by calling the block
|
|
196
|
+
#
|
|
197
|
+
# @example
|
|
198
|
+
# block_io.write("data")
|
|
199
|
+
#
|
|
200
|
+
# @param data [String]
|
|
201
|
+
# @return [Object]
|
|
202
|
+
# @api public
|
|
104
203
|
def write(data)
|
|
105
204
|
@block.call(data)
|
|
106
205
|
end
|
|
107
206
|
end
|
|
108
207
|
end
|
|
109
208
|
|
|
209
|
+
# Deflate-compressed request body wrapper
|
|
110
210
|
class DeflatedBody < CompressedBody
|
|
211
|
+
# Compresses data using deflate
|
|
212
|
+
#
|
|
213
|
+
# @example
|
|
214
|
+
# deflated_body.compress { |data| io.write(data) }
|
|
215
|
+
#
|
|
216
|
+
# @return [nil]
|
|
217
|
+
# @api public
|
|
111
218
|
def compress
|
|
112
219
|
deflater = Zlib::Deflate.new
|
|
113
220
|
|
|
@@ -1,35 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
3
|
module HTTP
|
|
6
4
|
module Features
|
|
5
|
+
# Automatically decompresses response bodies
|
|
7
6
|
class AutoInflate < Feature
|
|
8
7
|
SUPPORTED_ENCODING = Set.new(%w[deflate gzip x-gzip]).freeze
|
|
9
8
|
private_constant :SUPPORTED_ENCODING
|
|
10
9
|
|
|
10
|
+
# Wraps a response with an auto-inflating body
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# feature.wrap_response(response)
|
|
14
|
+
#
|
|
15
|
+
# @param response [HTTP::Response]
|
|
16
|
+
# @return [HTTP::Response]
|
|
17
|
+
# @api public
|
|
11
18
|
def wrap_response(response)
|
|
12
19
|
return response unless supported_encoding?(response)
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
:status => response.status,
|
|
16
|
-
:version => response.version,
|
|
17
|
-
:headers => response.headers,
|
|
18
|
-
:proxy_headers => response.proxy_headers,
|
|
19
|
-
:connection => response.connection,
|
|
20
|
-
:body => stream_for(response.connection),
|
|
21
|
-
:request => response.request
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
Response.new(options)
|
|
21
|
+
Response.new(**inflated_response_options(response)) # steep:ignore
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
# Returns an inflating body stream for a connection
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# feature.stream_for(connection)
|
|
28
|
+
#
|
|
29
|
+
# @param connection [HTTP::Connection]
|
|
30
|
+
# @param encoding [Encoding] encoding to use for the inflated body
|
|
31
|
+
# @return [HTTP::Response::Body]
|
|
32
|
+
# @api public
|
|
33
|
+
def stream_for(connection, encoding: Encoding::BINARY)
|
|
34
|
+
Response::Body.new(Response::Inflater.new(connection), encoding: encoding)
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
private
|
|
32
38
|
|
|
39
|
+
# Build options hash for an inflated response
|
|
40
|
+
# @return [Hash]
|
|
41
|
+
# @api private
|
|
42
|
+
def inflated_response_options(response)
|
|
43
|
+
{
|
|
44
|
+
status: response.status,
|
|
45
|
+
version: response.version,
|
|
46
|
+
headers: response.headers,
|
|
47
|
+
proxy_headers: response.proxy_headers,
|
|
48
|
+
connection: response.connection,
|
|
49
|
+
body: stream_for(response.connection, encoding: response.body.encoding),
|
|
50
|
+
request: response.request
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if the response encoding is supported
|
|
55
|
+
# @api private
|
|
33
56
|
def supported_encoding?(response)
|
|
34
57
|
content_encoding = response.headers.get(Headers::CONTENT_ENCODING).first
|
|
35
58
|
content_encoding && SUPPORTED_ENCODING.include?(content_encoding)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module HTTP
|
|
6
|
+
module Features
|
|
7
|
+
class Caching < Feature
|
|
8
|
+
# A cached response entry with freshness logic
|
|
9
|
+
class Entry
|
|
10
|
+
# The HTTP status code
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# entry.status # => 200
|
|
14
|
+
#
|
|
15
|
+
# @return [Integer] the HTTP status code
|
|
16
|
+
# @api public
|
|
17
|
+
attr_reader :status
|
|
18
|
+
|
|
19
|
+
# The HTTP version
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# entry.version # => "1.1"
|
|
23
|
+
#
|
|
24
|
+
# @return [String] the HTTP version
|
|
25
|
+
# @api public
|
|
26
|
+
attr_reader :version
|
|
27
|
+
|
|
28
|
+
# The response headers
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# entry.headers
|
|
32
|
+
#
|
|
33
|
+
# @return [HTTP::Headers] the response headers
|
|
34
|
+
# @api public
|
|
35
|
+
attr_reader :headers
|
|
36
|
+
|
|
37
|
+
# The proxy headers from the original response
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# entry.proxy_headers
|
|
41
|
+
#
|
|
42
|
+
# @return [HTTP::Headers] the proxy headers
|
|
43
|
+
# @api public
|
|
44
|
+
attr_reader :proxy_headers
|
|
45
|
+
|
|
46
|
+
# The response body as a string
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# entry.body # => "<html>...</html>"
|
|
50
|
+
#
|
|
51
|
+
# @return [String] the response body
|
|
52
|
+
# @api public
|
|
53
|
+
attr_reader :body
|
|
54
|
+
|
|
55
|
+
# The URI of the original request
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# entry.request_uri
|
|
59
|
+
#
|
|
60
|
+
# @return [HTTP::URI] the request URI
|
|
61
|
+
# @api public
|
|
62
|
+
attr_reader :request_uri
|
|
63
|
+
|
|
64
|
+
# When the response was stored
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# entry.stored_at
|
|
68
|
+
#
|
|
69
|
+
# @return [Time] when the response was stored
|
|
70
|
+
# @api public
|
|
71
|
+
attr_reader :stored_at
|
|
72
|
+
|
|
73
|
+
# Create a new cache entry
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# Entry.new(status: 200, version: "1.1", headers: headers,
|
|
77
|
+
# proxy_headers: proxy_headers, body: "hello",
|
|
78
|
+
# request_uri: uri, stored_at: Time.now)
|
|
79
|
+
#
|
|
80
|
+
# @param status [Integer]
|
|
81
|
+
# @param version [String]
|
|
82
|
+
# @param headers [HTTP::Headers]
|
|
83
|
+
# @param proxy_headers [HTTP::Headers]
|
|
84
|
+
# @param body [String]
|
|
85
|
+
# @param request_uri [HTTP::URI]
|
|
86
|
+
# @param stored_at [Time]
|
|
87
|
+
# @return [Entry]
|
|
88
|
+
# @api public
|
|
89
|
+
def initialize(status:, version:, headers:, proxy_headers:, body:, request_uri:, stored_at:)
|
|
90
|
+
@status = status
|
|
91
|
+
@version = version
|
|
92
|
+
@headers = headers
|
|
93
|
+
@proxy_headers = proxy_headers
|
|
94
|
+
@body = body
|
|
95
|
+
@request_uri = request_uri
|
|
96
|
+
@stored_at = stored_at
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Whether the cached response is still fresh
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# entry.fresh? # => true
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean]
|
|
105
|
+
# @api public
|
|
106
|
+
def fresh?
|
|
107
|
+
return false if no_cache?
|
|
108
|
+
|
|
109
|
+
ttl = max_age
|
|
110
|
+
return age < ttl if ttl
|
|
111
|
+
|
|
112
|
+
expires = expires_at
|
|
113
|
+
return Time.now < expires if expires
|
|
114
|
+
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Reset the stored_at time to now (after successful revalidation)
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# entry.revalidate!
|
|
122
|
+
#
|
|
123
|
+
# @return [Time]
|
|
124
|
+
# @api public
|
|
125
|
+
def revalidate!
|
|
126
|
+
@stored_at = Time.now
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Merge response headers from a 304 revalidation into the stored entry
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# entry.update_headers!(response.headers)
|
|
133
|
+
#
|
|
134
|
+
# @param response_headers [HTTP::Headers]
|
|
135
|
+
# @return [void]
|
|
136
|
+
# @api public
|
|
137
|
+
def update_headers!(response_headers)
|
|
138
|
+
response_headers.each { |name, value| @headers[name] = value } # steep:ignore
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Age of the entry in seconds
|
|
144
|
+
# @return [Float]
|
|
145
|
+
# @api private
|
|
146
|
+
def age
|
|
147
|
+
Float(Integer(headers[Headers::AGE], exception: false) || 0) + (Time.now - stored_at)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# max-age value from Cache-Control, if present
|
|
151
|
+
# @return [Integer, nil]
|
|
152
|
+
# @api private
|
|
153
|
+
def max_age
|
|
154
|
+
match = String(headers[Headers::CACHE_CONTROL]).match(/max-age=(\d+)/)
|
|
155
|
+
return unless match
|
|
156
|
+
|
|
157
|
+
Integer(match[1])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Expiration time from Expires header
|
|
161
|
+
# @return [Time, nil]
|
|
162
|
+
# @api private
|
|
163
|
+
def expires_at
|
|
164
|
+
Time.httpdate(String(headers[Headers::EXPIRES]))
|
|
165
|
+
rescue ArgumentError
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Whether the Cache-Control includes no-cache
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
# @api private
|
|
172
|
+
def no_cache?
|
|
173
|
+
String(headers[Headers::CACHE_CONTROL]).downcase.include?("no-cache")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
module Features
|
|
5
|
+
class Caching < Feature
|
|
6
|
+
# Simple in-memory cache store backed by a Hash
|
|
7
|
+
#
|
|
8
|
+
# Cache keys are derived from the request method and URI.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# store = InMemoryStore.new
|
|
12
|
+
# store.store(request, entry)
|
|
13
|
+
# store.lookup(request) # => entry
|
|
14
|
+
#
|
|
15
|
+
class InMemoryStore
|
|
16
|
+
# Create a new empty in-memory store
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# store = InMemoryStore.new
|
|
20
|
+
#
|
|
21
|
+
# @return [InMemoryStore]
|
|
22
|
+
# @api public
|
|
23
|
+
def initialize
|
|
24
|
+
@cache = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Look up a cached entry for a request
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# store.lookup(request) # => Entry or nil
|
|
31
|
+
#
|
|
32
|
+
# @param request [HTTP::Request]
|
|
33
|
+
# @return [Entry, nil]
|
|
34
|
+
# @api public
|
|
35
|
+
def lookup(request)
|
|
36
|
+
@cache[cache_key(request)]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Store a cache entry for a request
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# store.store(request, entry)
|
|
43
|
+
#
|
|
44
|
+
# @param request [HTTP::Request]
|
|
45
|
+
# @param entry [Entry]
|
|
46
|
+
# @return [Entry]
|
|
47
|
+
# @api public
|
|
48
|
+
def store(request, entry)
|
|
49
|
+
@cache[cache_key(request)] = entry
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Compute the cache key for a request
|
|
55
|
+
# @return [String]
|
|
56
|
+
# @api private
|
|
57
|
+
def cache_key(request)
|
|
58
|
+
format("%s %s", request.verb, request.uri)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|