From b463f60158ccf791809cb51cba2cf7a14e491b36 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 26 May 2018 15:17:33 -0700 Subject: [PATCH] ?_ttl= parameter and default_cache_ttl config Refs #285, Closes #289 --- datasette/app.py | 3 +++ datasette/views/base.py | 12 +++++++++++- docs/config.rst | 9 +++++++++ docs/json_api.rst | 4 ++++ tests/test_api.py | 12 ++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/datasette/app.py b/datasette/app.py index 6cc31b85..b048c67e 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -83,6 +83,9 @@ CONFIG_OPTIONS = ( ConfigOption("allow_sql", True, """ Allow arbitrary SQL queries via ?sql= parameter """.strip()), + ConfigOption("default_cache_ttl", 365 * 24 * 60 * 60, """ + Default HTTP cache TTL (used in Cache-Control: max-age= header) + """.strip()), ) DEFAULT_CONFIG = { option.name: option.default diff --git a/datasette/views/base.py b/datasette/views/base.py index f50cb90e..90aed924 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -257,7 +257,17 @@ class BaseView(RenderMixin): r.status = status_code # Set far-future cache expiry if self.ds.cache_headers: - r.headers["Cache-Control"] = "max-age={}".format(365 * 24 * 60 * 60) + ttl = request.args.get("_ttl", None) + if ttl is None or not ttl.isdigit(): + ttl = self.ds.config["default_cache_ttl"] + else: + ttl = int(ttl) + if ttl == 0: + ttl_header = 'no-cache' + else: + ttl_header = 'max-age={}'.format(ttl) + r.headers["Cache-Control"] = ttl_header + r.headers["Referrer-Policy"] = "no-referrer" return r async def custom_sql( diff --git a/docs/config.rst b/docs/config.rst index b3918e06..a8b466a5 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -92,3 +92,12 @@ allow_sql Enable/disable the ability for users to run custom SQL directly against a database. To disable this feature, run:: datasette mydatabase.db --config allow_sql:off + +default_cache_ttl +----------------- + +Default HTTP caching max-age header in seconds, used for ``Cache-Control: max-age=X``. Can be over-ridden on a per-request basis using the ``?_ttl=`` querystring parameter. Set this to ``0`` to disable HTTP caching entirely. Defaults to 365 days (31536000 seconds). + +:: + + datasette mydatabase.db --config default_cache_ttl:10 diff --git a/docs/json_api.rst b/docs/json_api.rst index 47c6f81b..c69eb9cc 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -164,6 +164,10 @@ The Datasette table view takes a number of special querystring arguments: long, for example if you want to implement autocomplete search but only if it can be executed in less than 10ms. +``?_ttl=SECONDS`` + For how many seconds should this response be cached by HTTP proxies? Use + ``?_ttl=0`` to disable HTTP caching entirely for this request. + ``?_next=TOKEN`` Pagination by continuation token - pass the token that was returned in the ``"next"`` property by the previous page. diff --git a/tests/test_api.py b/tests/test_api.py index e76762ad..051a7fdc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -928,6 +928,7 @@ def test_config_json(app_client): "allow_facet": True, "suggest_facets": True, "allow_sql": True, + "default_cache_ttl": 365 * 24 * 60 * 60, } == response.json @@ -1127,3 +1128,14 @@ def test_suggest_facets_off(): "/test_tables/facetable.json", gather_request=False ).json["suggested_facets"] + + +@pytest.mark.parametrize('path,expected_cache_control', [ + ("/test_tables/facetable.json", "max-age=31536000"), + ("/test_tables/facetable.json?_ttl=invalid", "max-age=31536000"), + ("/test_tables/facetable.json?_ttl=10", "max-age=10"), + ("/test_tables/facetable.json?_ttl=0", "no-cache"), +]) +def test_ttl_parameter(app_client, path, expected_cache_control): + response = app_client.get(path, gather_request=False) + assert expected_cache_control == response.headers['Cache-Control']