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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- 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 +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- 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 +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- 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/capture_warning.rb +10 -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/test/support/fakeio.rb +21 -0
- 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/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- 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
|
data/lib/http/errors.rb
ADDED
|
@@ -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
|
data/lib/http/feature.rb
ADDED
|
@@ -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
|