# Improving the Flask cache decorator

In my rumblings developing flask applications I found the caching decorator. The decorator is a short and well-written piece of code, but I feel like it misses the few points bellow:

• It only caches on the server side and doesn’t leverage the client-side cache - meaning that clients still have to go and hit the server every single time they require a resource
• It doesn’t respect the no-cache requirement from the client (although I understand why one may not want this, as it would mean busting cache every single time someone asks for it, thus creating the possibility of reducing the caching effort and benefits to zero)

The way I try to improve it is by having a decorator that easily allows me to define a caching-control policy per view, that involes both the client-side and server-side, by following the flow-chart bellow, taken from the HTTP Caching article by Google Guru Ilya Grigorik

The code for the decorator is:

import binascii
import datetime
def cached(cacheable = False, must_revalidate = True, client_only = True, client_timeout = 0, server_timeout = 5 * 60, key='view/%s'):
"""

@see https://jakearchibald.com/2016/caching-best-practices/
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
cache_key = key % request.full_path # include querystring
cache_policy = ''
etag = ''
if not cacheable:
cache_policy += ', no-store' # tells the browser not to cache at all
else:
if must_revalidate: # this looks contradicting if you haven't read the article.
# no-cache doesn't mean "don't cache", it means it must check
# (or "revalidate" as it calls it) with the server before
# using the cached resource
cache_policy += ', no-cache'
else:
# Also must-revalidate doesn't mean "must revalidate", it
# means the local resource can be used if it's younger than
# the provided max-age, otherwise it must revalidate
cache_policy += ', must-revalidate'

if client_only:
cache_policy += ', private'
else:
cache_policy += ', public'

cache_policy += ', max-age=%d' % (client_timeout)

cache_policy = cache_policy.strip(',')
now = datetime.datetime.utcnow()

response = cache.get(cache_key)
# respect the hard-refresh
if response is not None and request.headers.get('Cache-Control', '') != 'no-cache':
if client_etag and cached_etag and client_etag == cached_etag:
response = make_response('', 304)
else:
response = make_response(f(*args, **kwargs))
if response.status_code == 200 and request.method in ['GET', 'HEAD']:
# headers dict so they get cached too
# - If you can find any faster random algorithm go for it.
cache.set(cache_key, response, timeout=server_timeout)

return response
return decorated_function
return decorator


The decorator can then by used like

@pages.route('/')
@cached(True, must_revalidate=True, client_only=False, client_timeout=120, server_timeout=5*60)
def index():
"""Serve client-side application shell."""
return render_template('shell.html', model = get_default_model())


Note that the cache on the decorator is provided by Werkzeug and in this particular case its in-memory, but it could be redis, memcached and etc. since Werkzeug cache provides a common high-level API for them, while keeping implementation details on the low-level away from the developers eyes or worries.

I'd like to have your feedback on this, on whether it is a valuable improvement in comparison with the "standard" decorator, where it could be improved and simplified.