http 6.0.0-java

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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -0
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Parsed representation of a Content-Type header
5
+ class ContentType
6
+ # Pattern for extracting MIME type from Content-Type header
7
+ MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
8
+ # Pattern for extracting charset from Content-Type header
9
+ CHARSET_RE = /;\s*charset=([^;]+)/i
10
+
11
+ # MIME type of the content
12
+ #
13
+ # @example
14
+ # content_type.mime_type # => "text/html"
15
+ #
16
+ # @return [String, nil]
17
+ # @api public
18
+ attr_accessor :mime_type
19
+
20
+ # Character set of the content
21
+ #
22
+ # @example
23
+ # content_type.charset # => "utf-8"
24
+ #
25
+ # @return [String, nil]
26
+ # @api public
27
+ attr_accessor :charset
28
+
29
+ class << self
30
+ # Parse string and return ContentType object
31
+ #
32
+ # @example
33
+ # HTTP::ContentType.parse("text/html; charset=utf-8")
34
+ #
35
+ # @param [String] str content type header value
36
+ # @return [ContentType]
37
+ # @api public
38
+ def parse(str)
39
+ new mime_type(str), charset(str)
40
+ end
41
+
42
+ private
43
+
44
+ # Extract MIME type from header string
45
+ # @return [String, nil]
46
+ # @api private
47
+ def mime_type(str)
48
+ str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
49
+ end
50
+
51
+ # Extract charset from header string
52
+ # @return [String, nil]
53
+ # @api private
54
+ def charset(str)
55
+ str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
56
+ end
57
+ end
58
+
59
+ # Create a new ContentType instance
60
+ #
61
+ # @example
62
+ # HTTP::ContentType.new("text/html", "utf-8")
63
+ #
64
+ # @param [String, nil] mime_type MIME type
65
+ # @param [String, nil] charset character set
66
+ # @return [ContentType]
67
+ # @api public
68
+ def initialize(mime_type = nil, charset = nil)
69
+ @mime_type = mime_type
70
+ @charset = charset
71
+ end
72
+
73
+ # Pattern matching interface for matching against content type attributes
74
+ #
75
+ # @example
76
+ # case response.content_type
77
+ # in { mime_type: /json/ }
78
+ # "JSON content"
79
+ # end
80
+ #
81
+ # @param keys [Array<Symbol>, nil] keys to extract, or nil for all
82
+ # @return [Hash{Symbol => Object}]
83
+ # @api public
84
+ def deconstruct_keys(keys)
85
+ hash = { mime_type: @mime_type, charset: @charset }
86
+ keys ? hash.slice(*keys) : hash
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Generic error
5
+ class Error < StandardError; end
6
+
7
+ # Generic Connection error
8
+ class ConnectionError < Error; end
9
+
10
+ # Types of Connection errors
11
+ class ResponseHeaderError < ConnectionError; end
12
+ # Error raised when reading from a socket fails
13
+ class SocketReadError < ConnectionError; end
14
+ # Error raised when writing to a socket fails
15
+ class SocketWriteError < ConnectionError; end
16
+
17
+ # Generic Request error
18
+ class RequestError < Error; end
19
+
20
+ # Generic Response error
21
+ class ResponseError < Error; end
22
+
23
+ # Requested to do something when we're in the wrong state
24
+ class StateError < ResponseError; end
25
+
26
+ # When status code indicates an error
27
+ class StatusError < ResponseError
28
+ # The HTTP response that caused the error
29
+ #
30
+ # @example
31
+ # error.response
32
+ #
33
+ # @return [HTTP::Response]
34
+ # @api public
35
+ attr_reader :response
36
+
37
+ # Create a new StatusError from a response
38
+ #
39
+ # @example
40
+ # HTTP::StatusError.new(response)
41
+ #
42
+ # @param [HTTP::Response] response the response with error status
43
+ # @return [StatusError]
44
+ # @api public
45
+ def initialize(response)
46
+ @response = response
47
+
48
+ super("Unexpected status code #{response.code}")
49
+ end
50
+ end
51
+
52
+ # Raised when `Response#parse` fails due to any underlying reason (unexpected
53
+ # MIME type, or decoder fails). See `Exception#cause` for the original exception.
54
+ class ParseError < ResponseError; end
55
+
56
+ # Requested MimeType adapter not found.
57
+ class UnsupportedMimeTypeError < Error; end
58
+
59
+ # Generic Timeout error
60
+ class TimeoutError < Error; end
61
+
62
+ # Timeout when first establishing the connection
63
+ class ConnectTimeoutError < TimeoutError; end
64
+
65
+ # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
66
+ class HeaderError < Error; end
67
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Base class for HTTP client features (middleware)
5
+ class Feature
6
+ # Wraps an HTTP request
7
+ #
8
+ # @example
9
+ # feature.wrap_request(request)
10
+ #
11
+ # @param request [HTTP::Request]
12
+ # @return [HTTP::Request]
13
+ # @api public
14
+ def wrap_request(request)
15
+ request
16
+ end
17
+
18
+ # Wraps an HTTP response
19
+ #
20
+ # @example
21
+ # feature.wrap_response(response)
22
+ #
23
+ # @param response [HTTP::Response]
24
+ # @return [HTTP::Response]
25
+ # @api public
26
+ def wrap_response(response)
27
+ response
28
+ end
29
+
30
+ # Callback invoked before each request attempt
31
+ #
32
+ # Unlike {#wrap_request}, which is called once when the request is built,
33
+ # this hook is called before every attempt, including retries. Use it for
34
+ # per-attempt side effects like starting instrumentation spans.
35
+ #
36
+ # @example
37
+ # feature.on_request(request)
38
+ #
39
+ # @param _request [HTTP::Request]
40
+ # @return [nil]
41
+ # @api public
42
+ def on_request(_request); end
43
+
44
+ # Wraps the HTTP exchange for a single request attempt
45
+ #
46
+ # Called once per attempt (including retries), wrapping the send and
47
+ # receive cycle. The block performs the I/O and returns the response.
48
+ # Override this to add behavior that must span the entire exchange,
49
+ # such as instrumentation spans or circuit breakers.
50
+ #
51
+ # @example Timing a request
52
+ # def around_request(request)
53
+ # start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ # yield(request).tap { log_duration(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) }
55
+ # end
56
+ #
57
+ # @param request [HTTP::Request]
58
+ # @yield [HTTP::Request] the request to perform
59
+ # @yieldreturn [HTTP::Response]
60
+ # @return [HTTP::Response] must return the response from yield
61
+ # @api public
62
+ def around_request(request)
63
+ yield request
64
+ end
65
+
66
+ # Callback for request errors
67
+ #
68
+ # @example
69
+ # feature.on_error(request, error)
70
+ #
71
+ # @param _request [HTTP::Request]
72
+ # @param _error [Exception]
73
+ # @return [nil]
74
+ # @api public
75
+ def on_error(_request, _error); end
76
+ end
77
+ end
78
+
79
+ require "http/features/auto_inflate"
80
+ require "http/features/auto_deflate"
81
+ require "http/features/caching"
82
+ require "http/features/digest_auth"
83
+ require "http/features/instrumentation"
84
+ require "http/features/logging"
85
+ require "http/features/normalize_uri"
86
+ require "http/features/raise_error"
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "zlib"
5
+
6
+ require "http/request/body"
7
+
8
+ module HTTP
9
+ module Features
10
+ # Automatically compresses request bodies with gzip or deflate
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
22
+ attr_reader :method
23
+
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()
34
+
35
+ @method = String(method)
36
+
37
+ raise Error, "Only gzip and deflate methods are supported" unless VALID_METHODS.include?(@method)
38
+ end
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
48
+ def wrap_request(request)
49
+ return request unless method
50
+ return request if request.body.empty?
51
+
52
+ # We need to delete Content-Length header. It will be set automatically by HTTP::Request::Writer
53
+ request.headers.delete(Headers::CONTENT_LENGTH)
54
+ request.headers[Headers::CONTENT_ENCODING] = method
55
+
56
+ build_deflated_request(request)
57
+ end
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
67
+ def deflated_body(body)
68
+ case method
69
+ when "gzip"
70
+ GzippedBody.new(body)
71
+ when "deflate"
72
+ DeflatedBody.new(body)
73
+ end
74
+ end
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
+
93
+ HTTP::Options.register_feature(:auto_deflate, self)
94
+
95
+ # Base class for compressed request body wrappers
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
105
+ def initialize(uncompressed_body)
106
+ super(nil)
107
+ @body = uncompressed_body
108
+ @compressed = nil
109
+ end
110
+
111
+ # Returns the size of the compressed body
112
+ #
113
+ # @example
114
+ # compressed_body.size
115
+ #
116
+ # @return [Integer]
117
+ # @api public
118
+ def size
119
+ compress_all! unless @compressed
120
+ @compressed.size
121
+ end
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
130
+ def each(&block)
131
+ return to_enum(:each) unless block
132
+
133
+ if @compressed
134
+ compressed_each(&block)
135
+ else
136
+ compress(&block)
137
+ end
138
+
139
+ self
140
+ end
141
+
142
+ private
143
+
144
+ # Yield each chunk from compressed data
145
+ # @return [void]
146
+ # @api private
147
+ def compressed_each
148
+ while (data = @compressed.read(Connection::BUFFER_SIZE))
149
+ yield data
150
+ end
151
+ ensure
152
+ @compressed.close!
153
+ end
154
+
155
+ # Compress all data to a tempfile
156
+ # @return [void]
157
+ # @api private
158
+ def compress_all!
159
+ @compressed = Tempfile.new("http-compressed_body", binmode: true)
160
+ compress { |data| @compressed.write(data) }
161
+ @compressed.rewind
162
+ end
163
+ end
164
+
165
+ # Gzip-compressed request body wrapper
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
174
+ def compress(&block)
175
+ gzip = Zlib::GzipWriter.new(BlockIO.new(block))
176
+ @body.each { |chunk| gzip.write(chunk) }
177
+ ensure
178
+ gzip.finish
179
+ end
180
+
181
+ # IO adapter that delegates writes to a block
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
191
+ def initialize(block)
192
+ @block = block
193
+ end
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
203
+ def write(data)
204
+ @block.call(data)
205
+ end
206
+ end
207
+ end
208
+
209
+ # Deflate-compressed request body wrapper
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
218
+ def compress
219
+ deflater = Zlib::Deflate.new
220
+
221
+ @body.each { |chunk| yield deflater.deflate(chunk) }
222
+
223
+ yield deflater.finish
224
+ ensure
225
+ deflater.close
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Features
5
+ # Automatically decompresses response bodies
6
+ class AutoInflate < Feature
7
+ SUPPORTED_ENCODING = Set.new(%w[deflate gzip x-gzip]).freeze
8
+ private_constant :SUPPORTED_ENCODING
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
18
+ def wrap_response(response)
19
+ return response unless supported_encoding?(response)
20
+
21
+ Response.new(**inflated_response_options(response)) # steep:ignore
22
+ end
23
+
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)
35
+ end
36
+
37
+ private
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
56
+ def supported_encoding?(response)
57
+ content_encoding = response.headers.get(Headers::CONTENT_ENCODING).first
58
+ content_encoding && SUPPORTED_ENCODING.include?(content_encoding)
59
+ end
60
+
61
+ HTTP::Options.register_feature(:auto_inflate, self)
62
+ end
63
+ end
64
+ end
@@ -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