-
Notifications
You must be signed in to change notification settings - Fork 86
/
response.rb
235 lines (205 loc) · 8.15 KB
/
response.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# frozen_string_literal: true
require 'time'
require 'faraday/http_cache/cache_control'
module Faraday
class HttpCache < Faraday::Middleware
# Internal: A class to represent a response from a Faraday request.
# It decorates the response hash into a smarter object that queries
# the response headers and status informations about how the caching
# middleware should handle this specific response.
class Response
# Internal: List of status codes that can be cached:
# * 200 - 'OK'
# * 203 - 'Non-Authoritative Information'
# * 300 - 'Multiple Choices'
# * 301 - 'Moved Permanently'
# * 302 - 'Found'
# * 404 - 'Not Found'
# * 410 - 'Gone'
CACHEABLE_STATUS_CODES = [200, 203, 300, 301, 302, 307, 404, 410].freeze
# Internal: Gets the actual response Hash (status, headers and body).
attr_reader :payload
# Internal: Gets the 'Last-Modified' header from the headers Hash.
attr_reader :last_modified
# Internal: Gets the 'ETag' header from the headers Hash.
attr_reader :etag
# Internal: Initialize a new Response with the response payload from
# a Faraday request.
#
# payload - the response Hash returned by a Faraday request.
# :status - the status code from the response.
# :response_headers - a 'Hash' like object with the headers.
# :body - the response body.
def initialize(payload = {})
@now = Time.now
@payload = payload
wrap_headers!
ensure_date_header!
@last_modified = headers['Last-Modified']
@etag = headers['ETag']
end
# Internal: Checks the response freshness based on expiration headers.
# The calculated 'ttl' should be present and bigger than 0.
#
# Returns true if the response is fresh, otherwise false.
def fresh?
!cache_control.no_cache? && ttl && ttl > 0
end
# Internal: Checks if the Response returned a 'Not Modified' status.
#
# Returns true if the response status code is 304.
def not_modified?
@payload[:status] == 304
end
# Internal: Checks if the response can be cached by the client when the
# client is acting as a shared cache per RFC 2616. This is validated by
# the 'Cache-Control' directives, the response status code and it's
# freshness or validation status.
#
# Returns false if the 'Cache-Control' says that we can't store the
# response, or it can be stored in private caches only, or if isn't fresh
# or it can't be revalidated with the origin server. Otherwise, returns
# true.
def cacheable_in_shared_cache?
cacheable?(true)
end
# Internal: Checks if the response can be cached by the client when the
# client is acting as a private cache per RFC 2616. This is validated by
# the 'Cache-Control' directives, the response status code and it's
# freshness or validation status.
#
# Returns false if the 'Cache-Control' says that we can't store the
# response, or if isn't fresh or it can't be revalidated with the origin
# server. Otherwise, returns true.
def cacheable_in_private_cache?
cacheable?(false)
end
# Internal: Gets the response age in seconds.
#
# Returns the 'Age' header if present, or subtracts the response 'date'
# from the current time.
def age
(headers['Age'] || (@now - date)).to_i
end
# Internal: Calculates the 'Time to live' left on the Response.
#
# Returns the remaining seconds for the response, or nil the 'max_age'
# isn't present.
def ttl
max_age - age if max_age
end
# Internal: Parses the 'Date' header back into a Time instance.
#
# Returns the Time object.
def date
Time.httpdate(headers['Date'])
end
# Internal: Gets the response max age.
# The max age is extracted from one of the following:
# * The shared max age directive from the 'Cache-Control' header;
# * The max age directive from the 'Cache-Control' header;
# * The difference between the 'Expires' header and the response
# date.
#
# Returns the max age value in seconds or nil if all options above fails.
def max_age
cache_control.shared_max_age ||
cache_control.max_age ||
(expires && (expires - @now))
end
# Internal: Creates a new 'Faraday::Response', merging the stored
# response with the supplied 'env' object.
#
# Returns a new instance of a 'Faraday::Response' with the payload.
def to_response(env)
env.update(@payload)
Faraday::Response.new(env)
end
# Internal: Exposes a representation of the current
# payload that we can serialize and cache properly.
#
# Returns a 'Hash'.
def serializable_hash
prepare_to_cache
{
status: @payload[:status],
body: @payload[:body],
response_headers: @payload[:response_headers],
reason_phrase: @payload[:reason_phrase]
}
end
private
# Internal: Checks if this response can be revalidated.
#
# Returns true if the 'headers' contains a 'Last-Modified' or an 'ETag'
# entry.
def validateable?
headers.key?('Last-Modified') || headers.key?('ETag')
end
# Internal: The logic behind cacheable_in_private_cache? and
# cacheable_in_shared_cache? The logic is the same except for the
# treatment of the private Cache-Control directive.
def cacheable?(shared_cache)
return false if (cache_control.private? && shared_cache) || cache_control.no_store?
cacheable_status_code? && (validateable? || fresh?)
end
# Internal: Validates the response status against the
# `CACHEABLE_STATUS_CODES' constant.
#
# Returns true if the constant includes the response status code.
def cacheable_status_code?
CACHEABLE_STATUS_CODES.include?(@payload[:status])
end
# Internal: Gets the 'Expires' in a Time object.
#
# Returns the Time object, or nil if the header isn't present or isn't RFC 2616 compliant.
def expires
@expires ||= headers['Expires'] && Time.httpdate(headers['Expires']) rescue nil # rubocop:disable Style/RescueModifier
end
# Internal: Gets the 'CacheControl' object.
def cache_control
@cache_control ||= CacheControl.new(headers['Cache-Control'])
end
# Internal: Converts the headers 'Hash' into 'Faraday::Utils::Headers'.
# Faraday actually uses a Hash subclass, `Faraday::Utils::Headers` to
# store the headers hash. When retrieving a serialized response,
# the headers object is decoded as a 'Hash' instead of the actual
# 'Faraday::Utils::Headers' object, so we need to ensure that the
# 'response_headers' is always a 'Headers' instead of a plain 'Hash'.
#
# Returns nothing.
def wrap_headers!
headers = @payload[:response_headers]
@payload[:response_headers] = Faraday::Utils::Headers.new
@payload[:response_headers].update(headers) if headers
end
# Internal: Try to parse the Date header, if it fails set it to @now.
#
# Returns nothing.
def ensure_date_header!
date
rescue StandardError
headers['Date'] = @now.httpdate
end
# Internal: Gets the headers 'Hash' from the payload.
def headers
@payload[:response_headers]
end
# Internal: Prepares the response headers to be cached.
#
# It removes the 'Age' header if present to allow cached responses
# to continue aging while cached. It also normalizes the 'max-age'
# related headers if the 'Age' header is provided to ensure accuracy
# once the 'Age' header is removed.
#
# Returns nothing.
def prepare_to_cache
if headers.key? 'Age'
cache_control.normalize_max_ages(headers['Age'].to_i)
headers.delete 'Age'
headers['Cache-Control'] = cache_control.to_s
end
end
end
end
end