Flask: Add etag and last-modified helpers to improve cachability

Created on 25 Apr 2014  路  5Comments  路  Source: pallets/flask

I noticed that there are no helpers for Etag or Last-Modified based caching.

That way we could use

def json():
    etag(some_etag)
    last_modified(some_timestamp)

    some_response = do_expensive_stuff
    return some_response

which is _very_ useful when you implement an API with expensive operations which often get requests for the exactly same response. Add a caching proxy and and they are only expensive once.

Sinatra (Ruby) has those helpers: https://github.com/sinatra/sinatra/blob/v1.4.5/lib/sinatra/base.rb#L439-L554

Most helpful comment

What I did was use flask-cache to save the response and then created a cache_header decorator:

from datetime import datetime as dt, timedelta
from json import dumps
from flask import make_response, request, route
from app import cache


def jsonify(**kwargs):
    response = make_response(dumps(kwargs))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    response.headers['mimetype'] = 'application/json'
    response.last_modified = dt.utcnow()
    response.add_etag()
    return response


def cache_header(max_age, **ckwargs):
    def decorator(view):
        f = cache.cached(max_age, **ckwargs)(view)

        @wraps(f)
        def wrapper(*args, **wkwargs):
            response = f(*args, **wkwargs)
            response.cache_control.max_age = max_age
            response.cache_control.public = True
            extra = timedelta(seconds=max_age)
            response.expires = response.last_modified + extra
            return response.make_conditional(request)
        return wrapper

    return decorator


@route('/route')
@cache_header(TIMEOUT)
def route():
    result = do_expensive_stuff()
    return jsonify(result=result)    

CR #637

All 5 comments

You can use response.add_etag() on a response object:

def json():
    some_response = do_expensive_stuff()
    some_response = make_response(some_response)
    some_response.add_etag()
    return some_response

This could quite easily be turned into a decorator.

Maybe I did not make my point clear well enough. Surely, I know how to add a header to my response.
I want my application to respond with 304 if Etag or Last-Modified match. I _don't_ want to regenerate any response which is already cached somewhere. etag or last_modified should yield a 304-response if the cache's object is fresh and otherwise add those headers to the newly response.
That's also how it's done in the code I linked.

Not sure what exactly you are looking for. There is already support for handling conditional requests. http://werkzeug.pocoo.org/docs/wrappers/#werkzeug.wrappers.ETagResponseMixin.make_conditional

I think @fb is saying that if an e-tag has been set on the response and the request has an e-tag in it then flask should automatically return a not modified response.

In response to @fb:

I want my application to respond with 304 if Etag or Last-Modified match.

As per Armin's reply this can be done using:

def json():
    some_response = do_expensive_stuff()
    some_response = make_response(some_response)
    some_response.add_etag()
    return some_response.make_conditional()

I don't want to regenerate any response which is already cached somewhere. etag or last_modified should yield a 304-response if the cache's object is fresh and otherwise add those headers to the newly response.

But to add an e-tag you need to know what the response is, so either your etag is not reliant upon the response and thus isn't really an etag and can cause stale results to be used or it is reliant on the response and thus your goal of "not wanting to regenerate a response" won't be met. Supposing you can somehow generate the etag without actually generating the response then you could do something like this:

def json()
    some_response = make_response()
    some_response.set_etag('the-etag')
    some_response.make_conditional()
    if some_response.status_code = 304:
        return some_response
    some_response =  do_expensive_stuff()
    some_response.set_etag('the-etag')
    return some_response

These are very crude and I would make use of decorators but you get the rough idea

What I did was use flask-cache to save the response and then created a cache_header decorator:

from datetime import datetime as dt, timedelta
from json import dumps
from flask import make_response, request, route
from app import cache


def jsonify(**kwargs):
    response = make_response(dumps(kwargs))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    response.headers['mimetype'] = 'application/json'
    response.last_modified = dt.utcnow()
    response.add_etag()
    return response


def cache_header(max_age, **ckwargs):
    def decorator(view):
        f = cache.cached(max_age, **ckwargs)(view)

        @wraps(f)
        def wrapper(*args, **wkwargs):
            response = f(*args, **wkwargs)
            response.cache_control.max_age = max_age
            response.cache_control.public = True
            extra = timedelta(seconds=max_age)
            response.expires = response.last_modified + extra
            return response.make_conditional(request)
        return wrapper

    return decorator


@route('/route')
@cache_header(TIMEOUT)
def route():
    result = do_expensive_stuff()
    return jsonify(result=result)    

CR #637

Was this page helpful?
0 / 5 - 0 ratings