ruby-http-session 1.0.1

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.
@@ -0,0 +1,196 @@
1
+ class HTTP::Session
2
+ class Cache
3
+ # Parses a cache-control header and exposes the directives as a Hash.
4
+ # Directives that do not have values are set to +true+.
5
+ #
6
+ # Mostly borrowed from [rack-cache/lib/rack/cache/cache_control.rb](https://github.com/rack/rack-cache/blob/main/lib/rack/cache/cache_control.rb)
7
+ class CacheControl < Hash
8
+ def initialize(value = nil)
9
+ parse(value)
10
+ end
11
+
12
+ # Indicates that the response MAY be cached by any cache, even if it
13
+ # would normally be non-cacheable or cacheable only within a non-
14
+ # shared cache.
15
+ #
16
+ # A response may be considered public without this directive if the
17
+ # private directive is not set and the request does not include an
18
+ # Authorization header.
19
+ def public?
20
+ self["public"]
21
+ end
22
+
23
+ # Indicates that all or part of the response message is intended for
24
+ # a single user and MUST NOT be cached by a shared cache. This
25
+ # allows an origin server to state that the specified parts of the
26
+ # response are intended for only one user and are not a valid
27
+ # response for requests by other users. A private (non-shared) cache
28
+ # MAY cache the response.
29
+ #
30
+ # Note: This usage of the word private only controls where the
31
+ # response may be cached, and cannot ensure the privacy of the
32
+ # message content.
33
+ def private?
34
+ self["private"]
35
+ end
36
+
37
+ # When set in a response, a cache MUST NOT use the response to satisfy a
38
+ # subsequent request without successful revalidation with the origin
39
+ # server. This allows an origin server to prevent caching even by caches
40
+ # that have been configured to return stale responses to client requests.
41
+ #
42
+ # Note that this does not necessary imply that the response may not be
43
+ # stored by the cache, only that the cache cannot serve it without first
44
+ # making a conditional GET request with the origin server.
45
+ #
46
+ # When set in a request, the server MUST NOT use a cached copy for its
47
+ # response. This has quite different semantics compared to the no-cache
48
+ # directive on responses. When the client specifies no-cache, it causes
49
+ # an end-to-end reload, forcing each cache to update their cached copies.
50
+ def no_cache?
51
+ self["no-cache"]
52
+ end
53
+
54
+ # Indicates that the response MUST NOT be stored under any circumstances.
55
+ #
56
+ # The purpose of the no-store directive is to prevent the
57
+ # inadvertent release or retention of sensitive information (for
58
+ # example, on backup tapes). The no-store directive applies to the
59
+ # entire message, and MAY be sent either in a response or in a
60
+ # request. If sent in a request, a cache MUST NOT store any part of
61
+ # either this request or any response to it. If sent in a response,
62
+ # a cache MUST NOT store any part of either this response or the
63
+ # request that elicited it. This directive applies to both non-
64
+ # shared and shared caches. "MUST NOT store" in this context means
65
+ # that the cache MUST NOT intentionally store the information in
66
+ # non-volatile storage, and MUST make a best-effort attempt to
67
+ # remove the information from volatile storage as promptly as
68
+ # possible after forwarding it.
69
+ #
70
+ # The purpose of this directive is to meet the stated requirements
71
+ # of certain users and service authors who are concerned about
72
+ # accidental releases of information via unanticipated accesses to
73
+ # cache data structures. While the use of this directive might
74
+ # improve privacy in some cases, we caution that it is NOT in any
75
+ # way a reliable or sufficient mechanism for ensuring privacy. In
76
+ # particular, malicious or compromised caches might not recognize or
77
+ # obey this directive, and communications networks might be
78
+ # vulnerable to eavesdropping.
79
+ def no_store?
80
+ self["no-store"]
81
+ end
82
+
83
+ # The expiration time of an entity MAY be specified by the origin
84
+ # server using the expires header (see section 14.21). Alternatively,
85
+ # it MAY be specified using the max-age directive in a response. When
86
+ # the max-age cache-control directive is present in a cached response,
87
+ # the response is stale if its current age is greater than the age
88
+ # value given (in seconds) at the time of a new request for that
89
+ # resource. The max-age directive on a response implies that the
90
+ # response is cacheable (i.e., "public") unless some other, more
91
+ # restrictive cache directive is also present.
92
+ #
93
+ # If a response includes both an expires header and a max-age
94
+ # directive, the max-age directive overrides the expires header, even
95
+ # if the expires header is more restrictive. This rule allows an origin
96
+ # server to provide, for a given response, a longer expiration time to
97
+ # an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
98
+ # useful if certain HTTP/1.0 caches improperly calculate ages or
99
+ # expiration times, perhaps due to desynchronized clocks.
100
+ #
101
+ # Many HTTP/1.0 cache implementations will treat an expires value that
102
+ # is less than or equal to the response Date value as being equivalent
103
+ # to the cache-control response directive "no-cache". If an HTTP/1.1
104
+ # cache receives such a response, and the response does not include a
105
+ # cache-control header field, it SHOULD consider the response to be
106
+ # non-cacheable in order to retain compatibility with HTTP/1.0 servers.
107
+ #
108
+ # When the max-age directive is included in the request, it indicates
109
+ # that the client is willing to accept a response whose age is no
110
+ # greater than the specified time in seconds.
111
+ def max_age
112
+ self["max-age"].to_i if key?("max-age")
113
+ end
114
+
115
+ # If a response includes an s-maxage directive, then for a shared
116
+ # cache (but not for a private cache), the maximum age specified by
117
+ # this directive overrides the maximum age specified by either the
118
+ # max-age directive or the expires header. The s-maxage directive
119
+ # also implies the semantics of the proxy-revalidate directive. i.e.,
120
+ # that the shared cache must not use the entry after it becomes stale
121
+ # to respond to a subsequent request without first revalidating it with
122
+ # the origin server. The s-maxage directive is always ignored by a
123
+ # private cache.
124
+ def shared_max_age
125
+ self["s-maxage"].to_i if key?("s-maxage")
126
+ end
127
+ alias_method :s_maxage, :shared_max_age
128
+
129
+ # Because a cache MAY be configured to ignore a server's specified
130
+ # expiration time, and because a client request MAY include a max-
131
+ # stale directive (which has a similar effect), the protocol also
132
+ # includes a mechanism for the origin server to require revalidation
133
+ # of a cache entry on any subsequent use. When the must-revalidate
134
+ # directive is present in a response received by a cache, that cache
135
+ # MUST NOT use the entry after it becomes stale to respond to a
136
+ # subsequent request without first revalidating it with the origin
137
+ # server. (I.e., the cache MUST do an end-to-end revalidation every
138
+ # time, if, based solely on the origin server's expires or max-age
139
+ # value, the cached response is stale.)
140
+ #
141
+ # The must-revalidate directive is necessary to support reliable
142
+ # operation for certain protocol features. In all circumstances an
143
+ # HTTP/1.1 cache MUST obey the must-revalidate directive; in
144
+ # particular, if the cache cannot reach the origin server for any
145
+ # reason, it MUST generate a 504 (Gateway Timeout) response.
146
+ #
147
+ # Servers SHOULD send the must-revalidate directive if and only if
148
+ # failure to revalidate a request on the entity could result in
149
+ # incorrect operation, such as a silently unexecuted financial
150
+ # transaction. Recipients MUST NOT take any automated action that
151
+ # violates this directive, and MUST NOT automatically provide an
152
+ # unvalidated copy of the entity if revalidation fails.
153
+ def must_revalidate?
154
+ self["must-revalidate"]
155
+ end
156
+
157
+ # The proxy-revalidate directive has the same meaning as the must-
158
+ # revalidate directive, except that it does not apply to non-shared
159
+ # user agent caches. It can be used on a response to an
160
+ # authenticated request to permit the user's cache to store and
161
+ # later return the response without needing to revalidate it (since
162
+ # it has already been authenticated once by that user), while still
163
+ # requiring proxies that service many users to revalidate each time
164
+ # (in order to make sure that each user has been authenticated).
165
+ # Note that such authenticated responses also need the public cache
166
+ # control directive in order to allow them to be cached at all.
167
+ def proxy_revalidate?
168
+ self["proxy-revalidate"]
169
+ end
170
+
171
+ def to_s
172
+ bools, vals = [], []
173
+ each do |key, value|
174
+ if value == true
175
+ bools << key
176
+ elsif value
177
+ vals << "#{key}=#{value}"
178
+ end
179
+ end
180
+ (bools.sort + vals.sort).join(", ")
181
+ end
182
+
183
+ private
184
+
185
+ def parse(value)
186
+ return if value.nil? || value.empty?
187
+ value.delete(" ").split(",").each do |part|
188
+ next if part.empty?
189
+ name, value = part.split("=", 2)
190
+ self[name.downcase] = (value || true) unless name.empty?
191
+ end
192
+ self
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,71 @@
1
+ class HTTP::Session
2
+ class Cache
3
+ class Entry
4
+ class << self
5
+ # Deserializes from a JSON primitive type.
6
+ def deserialize(h)
7
+ req = h[:req]
8
+ req = HTTP::Session::Request.new(HTTP::Request.new(req))
9
+
10
+ res = h[:res]
11
+ res[:request] = req
12
+ res[:body] = HTTP::Session::Response::StringBody.new(res[:body])
13
+ res = HTTP::Session::Response.new(HTTP::Response.new(res))
14
+
15
+ new(req, res)
16
+ end
17
+ end
18
+
19
+ # @!attribute [r] request
20
+ # @return [Request]
21
+ attr_reader :request
22
+
23
+ # @!attribute [r] response
24
+ # @return [Response]
25
+ attr_reader :response
26
+
27
+ # Returns a new instance of Entry.
28
+ def initialize(req, res)
29
+ @request = req
30
+ @response = res
31
+ end
32
+
33
+ # @param [Request] req │
34
+ # @return [Response]
35
+ def to_response(req)
36
+ h = serialize_response
37
+ h[:request] = req
38
+ h[:body] = HTTP::Session::Response::StringBody.new(h[:body])
39
+ HTTP::Session::Response.new(HTTP::Response.new(h))
40
+ end
41
+
42
+ # Serializes to a JSON primitive type.
43
+ def serialize
44
+ {
45
+ req: serialize_request,
46
+ res: serialize_response
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def serialize_request
53
+ {
54
+ verb: @request.verb,
55
+ uri: @request.uri.to_s,
56
+ headers: @request.headers.to_h
57
+ }
58
+ end
59
+
60
+ def serialize_response
61
+ {
62
+ status: @response.status.code,
63
+ version: @response.version,
64
+ headers: @response.headers.to_h,
65
+ proxy_headers: @response.proxy_headers.to_h,
66
+ body: @response.body.to_s
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ class HTTP::Session
2
+ class Cache
3
+ class Status
4
+ # rubocop:disable Layout/ExtraSpacing
5
+ HEADER_NAME = "X-Httprb-Cache-Status"
6
+ HIT = "HIT" # found in cache
7
+ REVALIDATED = "REVALIDATED" # found in cache but stale, revalidated success
8
+ EXPIRED = "EXPIRED" # found in cache but stale, revalidated failure, served from the origin server
9
+ MISS = "MISS" # not found in cache, served from the origin server
10
+ UNCACHEABLE = "UNCACHEABLE" # the request can not use cached response
11
+ # rubocop:enable Layout/ExtraSpacing
12
+
13
+ class << self
14
+ def HIT?(v)
15
+ v == HIT || v == REVALIDATED
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,95 @@
1
+ class HTTP::Session
2
+ class Cache
3
+ include MonitorMixin
4
+
5
+ # @param [Options::CacheOption] options
6
+ def initialize(options)
7
+ super()
8
+
9
+ @options = options
10
+ end
11
+
12
+ # Read an entry from cache.
13
+ #
14
+ # @param [Request] req
15
+ # @return [Entry]
16
+ def read(req)
17
+ synchronize do
18
+ key = cache_key_for(req)
19
+ entries = read_entries(key)
20
+ entries.find { |e| entry_matched?(e, req) }
21
+ end
22
+ end
23
+
24
+ # Write an entry to cache.
25
+ #
26
+ # @param [Request] req
27
+ # @param [Response] res
28
+ # @return [void]
29
+ def write(req, res)
30
+ synchronize do
31
+ key = cache_key_for(req)
32
+ entries = read_entries(key)
33
+ entries = entries.reject { |e| entry_matched?(e, req) }
34
+ entry = HTTP::Session::Cache::Entry.new(req, res)
35
+ entries << entry
36
+ write_entries(key, entries)
37
+ end
38
+ end
39
+
40
+ # True when it is a shared cache.
41
+ def shared?
42
+ @options.shared_cache?
43
+ end
44
+
45
+ # True when it is a private cache.
46
+ def private?
47
+ @options.private_cache?
48
+ end
49
+
50
+ # @!visibility private
51
+ def store
52
+ @options.store
53
+ end
54
+
55
+ private
56
+
57
+ def entry_matched?(entry, req)
58
+ entry_matched_by_verb?(entry, req) &&
59
+ entry_matched_by_headers?(entry, req)
60
+ end
61
+
62
+ def entry_matched_by_verb?(entry, req)
63
+ entry.request.verb == req.verb
64
+ end
65
+
66
+ def entry_matched_by_headers?(entry, req)
67
+ vary = entry.response.headers[HTTP::Headers::VARY]
68
+ return true if vary.nil? || vary == ""
69
+
70
+ vary != "*" && vary.split(",").map(&:strip).all? do |name|
71
+ entry.request.headers[name] == req.headers[name]
72
+ end
73
+ end
74
+
75
+ def read_entries(key)
76
+ entrie = store.read(key) || []
77
+ entrie.map do |e|
78
+ Entry.deserialize(e)
79
+ end
80
+ end
81
+
82
+ def write_entries(key, entries)
83
+ entries = entries.map do |e|
84
+ e = e.serialize
85
+ e[:res][:headers].delete(HTTP::Headers::AGE)
86
+ e
87
+ end
88
+ store.write(key, entries)
89
+ end
90
+
91
+ def cache_key_for(req)
92
+ Digest::SHA256.hexdigest(req.uri)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,143 @@
1
+ class HTTP::Session
2
+ class Client
3
+ # Make an HTTP request without any HTTP::Features.
4
+ #
5
+ # Mostly borrowed from [http/lib/http/client.rb](https://github.com/httprb/http/blob/main/lib/http/client.rb)
6
+ module Perform
7
+ HTTP_OR_HTTPS_RE = %r{^https?://}i
8
+
9
+ attr_reader :default_options
10
+
11
+ def httprb_initialize(default_options)
12
+ @default_options = HTTP::Options.new(default_options)
13
+ @connection = nil
14
+ @state = :clean
15
+ end
16
+
17
+ def httprb_perform(req, options)
18
+ verify_connection!(req.uri)
19
+
20
+ @state = :dirty
21
+ begin
22
+ @connection ||= HTTP::Connection.new(req, options)
23
+ unless @connection.failed_proxy_connect?
24
+ @connection.send_request(req)
25
+ @connection.read_headers!
26
+ end
27
+ rescue HTTP::Error => e
28
+ options.features.each_value do |feature|
29
+ feature.on_error(req, e)
30
+ end
31
+ raise
32
+ end
33
+
34
+ res = build_response(req, options)
35
+ @connection.finish_response if req.verb == :head
36
+ @state = :clean
37
+ res
38
+ rescue
39
+ close
40
+ raise
41
+ end
42
+
43
+ private
44
+
45
+ def verify_connection!(uri)
46
+ if default_options.persistent? && uri.origin != default_options.persistent
47
+ raise HTTP::StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{uri.origin}"
48
+ end
49
+ return close if @connection && (!@connection.keep_alive? || @connection.expired?)
50
+ close if @state == :dirty
51
+ end
52
+
53
+ def close
54
+ @connection&.close
55
+ @connection = nil
56
+ @state = :clean
57
+ end
58
+
59
+ def build_response(req, options)
60
+ res = HTTP::Response.new(
61
+ status: @connection.status_code,
62
+ version: @connection.http_version,
63
+ headers: @connection.headers,
64
+ proxy_headers: @connection.proxy_response_headers,
65
+ connection: @connection,
66
+ encoding: options.encoding,
67
+ request: req
68
+ )
69
+ HTTP::Session::Response.new(res)
70
+ end
71
+
72
+ def wrap_response(res, opts)
73
+ opts.features.to_a.reverse.to_h.inject(res) do |response, (_name, feature)|
74
+ response = feature.wrap_response(response)
75
+ HTTP::Session::Response.new(response)
76
+ end
77
+ end
78
+
79
+ def build_request(verb, uri, opts = {})
80
+ opts = @default_options.merge(opts)
81
+ uri = make_request_uri(uri, opts)
82
+ headers = make_request_headers(opts)
83
+ body = make_request_body(opts, headers)
84
+ req = HTTP::Request.new(
85
+ verb: verb,
86
+ uri: uri,
87
+ uri_normalizer: opts.feature(:normalize_uri)&.normalizer,
88
+ proxy: opts.proxy,
89
+ headers: headers,
90
+ body: body
91
+ )
92
+ HTTP::Session::Request.new(req)
93
+ end
94
+
95
+ def wrap_request(req, opts)
96
+ opts.features.inject(req) do |request, (_name, feature)|
97
+ request = feature.wrap_request(request)
98
+ HTTP::Session::Request.new(request)
99
+ end
100
+ end
101
+
102
+ def make_request_uri(uri, opts)
103
+ uri = uri.to_s
104
+ uri = "#{default_options.persistent}#{uri}" if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE
105
+ uri = HTTP::URI.parse uri
106
+ uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a) if opts.params && !opts.params.empty?
107
+ uri.path = "/" if uri.path.empty?
108
+ uri
109
+ end
110
+
111
+ def make_request_headers(opts)
112
+ headers = opts.headers
113
+ headers[HTTP::Headers::CONNECTION] = default_options.persistent? ? HTTP::Connection::KEEP_ALIVE : HTTP::Connection::CLOSE
114
+ cookies = opts.cookies.values
115
+ unless cookies.empty?
116
+ cookies = opts.headers.get(HTTP::Headers::COOKIE).concat(cookies).join("; ")
117
+ headers[HTTP::Headers::COOKIE] = cookies
118
+ end
119
+ headers
120
+ end
121
+
122
+ def make_request_body(opts, headers)
123
+ if opts.body
124
+ opts.body
125
+ elsif opts.form
126
+ form = make_form_data(opts.form)
127
+ headers[HTTP::Headers::CONTENT_TYPE] ||= form.content_type
128
+ form
129
+ elsif opts.json
130
+ body = HTTP::MimeType[:json].encode opts.json
131
+ headers[HTTP::Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
132
+ body
133
+ end
134
+ end
135
+
136
+ def make_form_data(form)
137
+ return form if form.is_a? HTTP::FormData::Multipart
138
+ return form if form.is_a? HTTP::FormData::Urlencoded
139
+ HTTP::FormData.create(form)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,157 @@
1
+ class HTTP::Session
2
+ class Client
3
+ include HTTP::Session::Client::Perform
4
+
5
+ # @param [Hash] default_options
6
+ # @param [Session] session
7
+ def initialize(default_options, session)
8
+ httprb_initialize(default_options)
9
+ @session = session
10
+ end
11
+
12
+ # @param verb
13
+ # @param uri
14
+ # @param [Hash] opts
15
+ # @return [Response]
16
+ def request(verb, uri, opts)
17
+ data = @session.make_http_request_data
18
+ hist = []
19
+
20
+ opts = @default_options.merge(opts)
21
+ opts = _hs_handle_http_request_options_cookies(opts, data[:cookies])
22
+ opts = _hs_handle_http_request_options_follow(opts, hist)
23
+
24
+ req = build_request(verb, uri, opts)
25
+ res = perform(req, opts)
26
+ return res unless opts.follow
27
+
28
+ HTTP::Redirector.new(opts.follow).perform(req, res) do |request|
29
+ request = HTTP::Session::Request.new(request)
30
+ perform(request, opts)
31
+ end.tap do |res|
32
+ res.history = hist
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Make an HTTP request.
39
+ #
40
+ # @param [Request] req
41
+ # @param [HTTP::Options] opts
42
+ # @return [Response]
43
+ def perform(req, opts)
44
+ req = wrap_request(req, opts)
45
+ wrap_response(_hs_perform(req, opts), opts)
46
+ end
47
+
48
+ # Add session cookie to the request's :cookies.
49
+ def _hs_handle_http_request_options_cookies(opts, cookies)
50
+ return opts if cookies.nil?
51
+ opts.with_cookies(cookies)
52
+ end
53
+
54
+ # Wrap the :on_redirect method in the request's :follow.
55
+ def _hs_handle_http_request_options_follow(opts, hist)
56
+ return opts unless opts.follow
57
+
58
+ follow = (opts.follow == true) ? {} : opts.follow
59
+ opts.with_follow(follow.merge(
60
+ on_redirect: _hs_handle_http_request_options_follow_hijack(follow[:on_redirect], hist)
61
+ ))
62
+ end
63
+
64
+ # Wrap the :on_redirect method.
65
+ def _hs_handle_http_request_options_follow_hijack(fn, hist)
66
+ lambda do |res, req|
67
+ hist << res
68
+ fn.call(res, req) if fn.respond_to?(:call)
69
+ end
70
+ end
71
+
72
+ # Make an HTTP request using cache.
73
+ def _hs_perform(req, opts)
74
+ if @session.default_options.cache.enabled?
75
+ req.cacheable? ? _hs_cache_lookup(req, opts) : _hs_cache_pass(req, opts)
76
+ else
77
+ _hs_forward(req, opts)
78
+ end
79
+ end
80
+
81
+ # Try to serve the response from cache.
82
+ #
83
+ # * When a matching cache entry is found and is fresh, use it as the response
84
+ # without forwarding any request to the backend.
85
+ # * When a matching cache entry is found but is stale, attempt to validate the
86
+ # entry with the backend using conditional GET.
87
+ # * When no matching cache entry is found, trigger miss processing.
88
+ def _hs_cache_lookup(req, opts)
89
+ entry = @session.cache.read(req)
90
+ if entry.nil?
91
+ _hs_cache_fetch(req, opts)
92
+ elsif entry.response.fresh?(shared: @session.cache.shared?) &&
93
+ !entry.response.no_cache? &&
94
+ !req.no_cache?
95
+ _hs_cache_reuse(req, opts, entry)
96
+ else
97
+ _hs_cache_validate(req, opts, entry)
98
+ end
99
+ end
100
+
101
+ # The cache entry is missing. Forward the request to the backend and determine
102
+ # whether the response should be stored.
103
+ def _hs_cache_fetch(req, opts)
104
+ res = _hs_forward(req, opts)
105
+ _hs_cache_entry_store(req, res)
106
+
107
+ res.headers[HTTP::Session::Cache::Status::HEADER_NAME] = HTTP::Session::Cache::Status::MISS
108
+ res
109
+ end
110
+
111
+ # The cache entry is fresh, reuse it.
112
+ def _hs_cache_reuse(req, opts, entry)
113
+ res = entry.to_response(req)
114
+ res.headers[HTTP::Headers::AGE] = [(res.now - res.date).to_i, 0].max.to_s
115
+ res.headers[HTTP::Session::Cache::Status::HEADER_NAME] = HTTP::Session::Cache::Status::HIT
116
+ res
117
+ end
118
+
119
+ # The cache entry is stale, revalidate it. The original request is used
120
+ # as a template for a conditional GET request with the backend.
121
+ def _hs_cache_validate(req, opts, entry)
122
+ req.headers[HTTP::Headers::IF_MODIFIED_SINCE] = entry.response.last_modified if entry.response.last_modified
123
+ req.headers[HTTP::Headers::IF_NONE_MATCH] = entry.response.etag if entry.response.etag
124
+
125
+ res = _hs_forward(req, opts)
126
+ if res.status.not_modified?
127
+ res = entry.to_response(req)
128
+ res.headers[HTTP::Session::Cache::Status::HEADER_NAME] = HTTP::Session::Cache::Status::REVALIDATED
129
+ return res
130
+ end
131
+
132
+ _hs_cache_entry_store(req, res)
133
+ res.headers[HTTP::Session::Cache::Status::HEADER_NAME] = HTTP::Session::Cache::Status::EXPIRED
134
+ res
135
+ end
136
+
137
+ # The request is not cacheable. So the request is sent to the backend, and the
138
+ # backend's response is sent to the client, but is not entered into the cache.
139
+ def _hs_cache_pass(req, opts)
140
+ res = _hs_forward(req, opts)
141
+ res.headers[HTTP::Session::Cache::Status::HEADER_NAME] = HTTP::Session::Cache::Status::UNCACHEABLE
142
+ res
143
+ end
144
+
145
+ # Store the response to cache.
146
+ def _hs_cache_entry_store(req, res)
147
+ if res.cacheable?(shared: @session.cache.shared?, req: req)
148
+ @session.cache.write(req, res)
149
+ end
150
+ end
151
+
152
+ # Delegate the request to the backend and create the response.
153
+ def _hs_forward(req, opts)
154
+ httprb_perform(req, opts)
155
+ end
156
+ end
157
+ end