Class: Async::HTTP::Cache::General

Inherits:
Protocol::HTTP::Middleware
  • Object
show all
Defined in:
lib/async/http/cache/general.rb

Overview

Implements a general shared cache according to www.rfc-editor.org/rfc/rfc9111

Constant Summary collapse

CACHE_CONTROL =
"cache-control"
CONTENT_TYPE =
"content-type"
AUTHORIZATION =
"authorization"
"cookie"
"set-cookie"
CACHEABLE_RESPONSE_CODES =

Status codes of responses that MAY be stored by a cache or used in reply to a subsequent request.

tools.ietf.org/html/rfc2616#section-13.4

{
	200 => true, # OK
	203 => true, # Non-Authoritative Information
	300 => true, # Multiple Choices
	301 => true, # Moved Permanently
	302 => true, # Found
	404 => true, # Not Found
	410 => true  # Gone
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, store: Store.default) ⇒ General

Initialize a new cache middleware instance.



43
44
45
46
47
48
49
# File 'lib/async/http/cache/general.rb', line 43

def initialize(app, store: Store.default)
	super(app)
	
	@count = 0
	
	@store = store
end

Instance Attribute Details

#countObject (readonly)

Returns the value of attribute count.



51
52
53
# File 'lib/async/http/cache/general.rb', line 51

def count
  @count
end

#storeObject (readonly)

Returns the value of attribute store.



52
53
54
# File 'lib/async/http/cache/general.rb', line 52

def store
  @store
end

Instance Method Details

#cacheable_request?(request) ⇒ Boolean

Determine if a request is cacheable based on method, headers, and body.

Returns:

  • (Boolean)


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
# File 'lib/async/http/cache/general.rb', line 73

def cacheable_request?(request)
	# We don't support caching requests which have a request body:
	if request.body
		return false
	end
	
	# We can't cache upgraded requests:
	if request.protocol
		return false
	end
	
	# We only support caching GET and HEAD requests:
	unless request.method == "GET" || request.method == "HEAD"
		return false
	end
	
	if request.headers[AUTHORIZATION]
		return false
	end
	
	if request.headers[COOKIE]
		return false
	end
	
	# Otherwise, we can cache it:
	return true
end

#cacheable_response?(response) ⇒ Boolean

Determine if a response is cacheable based on status code and headers.

Returns:

  • (Boolean)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/async/http/cache/general.rb', line 123

def cacheable_response?(response)
	# At this point, we know response.status and response.headers.
	# But we don't know response.body or response.headers.trailer.
	unless CACHEABLE_RESPONSE_CODES.include?(response.status)
		Console.logger.debug(self, status: response.status) {"Cannot cache response with status code!"}
		return false
	end
	
	unless cacheable_response_headers?(response.headers)
		Console.logger.debug(self) {"Cannot cache response with uncacheable headers!"}
		return false
	end
	
	return true
end

#cacheable_response_headers?(headers) ⇒ Boolean

Check if response headers allow caching.

Returns:

  • (Boolean)


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/async/http/cache/general.rb', line 104

def cacheable_response_headers?(headers)
	if cache_control = headers[CACHE_CONTROL]
		if cache_control.no_store? || cache_control.private?
			Console.logger.debug(self, cache_control: cache_control) {"Cannot cache response with cache-control header!"}
			return false
		end
	end
	
	if set_cookie = headers[SET_COOKIE]
		Console.logger.debug(self) {"Cannot cache response with set-cookie header!"}
		return false
	end
	
	return true
end

#call(request) ⇒ Object

Process an HTTP request, checking cache first and storing responses when appropriate.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/async/http/cache/general.rb', line 184

def call(request)
	cache_control = request.headers[CACHE_CONTROL]
	
	unless cache_control&.no_cache?
		key = self.key(request)
		
		if response = @store.lookup(key, request)
			Console.logger.debug(self, key: key) {"Cache hit!"}
			@count += 1
			
			# Return the cached response:
			return response
		end
	end
	
	unless cache_control&.no_store?
		return wrap(key, request, super)
	end
	
	return super
end

#closeObject

Close the cache and clean up resources.



55
56
57
58
59
# File 'lib/async/http/cache/general.rb', line 55

def close
	@store.close
ensure
	super
end

#key(request) ⇒ Object

Generate a cache key for the given request.



64
65
66
67
68
# File 'lib/async/http/cache/general.rb', line 64

def key(request)
	@store.normalize(request)
	
	[request.authority, request.method, request.path]
end

#proceed_with_response_cache?(response) ⇒ Boolean

Semantically speaking, it is possible for trailers to result in an uncacheable response, so we need to check for that.

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
148
149
# File 'lib/async/http/cache/general.rb', line 140

def proceed_with_response_cache?(response)
	if response.headers.trailer?
		unless cacheable_response_headers?(response.headers)
			Console.logger.debug(self, trailer: trailer.keys) {"Cannot cache response with trailer header!"}
			return false
		end
	end
	
	return true
end

#wrap(key, request, response) ⇒ Object

Potentially wrap the response so that it updates the cache, if caching is possible.



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
# File 'lib/async/http/cache/general.rb', line 152

def wrap(key, request, response)
	if request.head? and body = response.body
		unless body.empty?
			Console.logger.warn(self) {"HEAD request resulted in non-empty body!"}
			
			return response
		end
	end
	
	unless cacheable_request?(request)
		Console.logger.debug(self) {"Cannot cache request!"}
		return response
	end
	
	unless cacheable_response?(response)
		Console.logger.debug(self) {"Cannot cache response!"}
		return response
	end
	
	return Body.wrap(response) do |response, body|
		if proceed_with_response_cache?(response)
			key ||= self.key(request)
			
			Console.logger.debug(self, key: key) {"Updating miss!"}
			@store.insert(key, request, Response.new(response, body))
		end
	end
end