prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  `Cache-Control: immutable`
  It's [a new experimental HTTP feature](https://bitsup.blogspot.fr/2016/05/cache-control-immutable.html) that we should probably start using. It's low priority though.
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `liberapay/utils/http_caching.py`
  Content:
  ```
  1 """
  2 Handles HTTP caching.
  3 """
  4 
  5 import atexit
  6 from hashlib import md5
  7 import os
  8 from tempfile import mkstemp
  9 
  10 from aspen.request_processor.dispatcher import DispatchResult, DispatchStatus
  11 from pando import Response
  12 
  13 from liberapay.utils import b64encode_s, find_files
  14 
  15 
  16 ETAGS = {}
  17 
  18 
  19 def compile_assets(website):
  20     cleanup = []
  21     for spt in find_files(website.www_root+'/assets/', '*.spt'):
  22         filepath = spt[:-4]  # /path/to/www/assets/foo.css
  23         if not os.path.exists(filepath):
  24             cleanup.append(filepath)
  25         dispatch_result = DispatchResult(DispatchStatus.okay, spt, None, None, None)
  26         state = dict(dispatch_result=dispatch_result, response=Response())
  27         state['state'] = state
  28         resource = website.request_processor.resources.get(spt)
  29         content = resource.render(state, dispatch_result, None).body
  30         if not isinstance(content, bytes):
  31             content = content.encode('utf8')
  32         tmpfd, tmpfpath = mkstemp(dir='.')
  33         os.write(tmpfd, content)
  34         os.close(tmpfd)
  35         os.rename(tmpfpath, filepath)
  36     if website.env.clean_assets:
  37         atexit.register(lambda: rm_f(*cleanup))
  38 
  39 
  40 def rm_f(*paths):
  41     for path in paths:
  42         try:
  43             os.unlink(path)
  44         except Exception:
  45             pass
  46 
  47 
  48 def clean_assets(www_root):
  49     rm_f(*[spt[:-4] for spt in find_files(www_root+'/assets/', '*.spt')])
  50 
  51 
  52 def asset_etag(path):
  53     if path.endswith('.spt'):
  54         return ''
  55     if path in ETAGS:
  56         return ETAGS[path]
  57     with open(path, 'rb') as f:
  58         h = b64encode_s(md5(f.read()).digest())
  59     ETAGS[path] = h
  60     return h
  61 
  62 
  63 # algorithm functions
  64 
  65 def get_etag_for_file(dispatch_result, website, state):
  66     if dispatch_result.status != DispatchStatus.okay:
  67         return {'etag': None}
  68     try:
  69         return {'etag': asset_etag(dispatch_result.match)}
  70     except Exception as e:
  71         website.tell_sentry(e, state)
  72         return {'etag': None}
  73 
  74 
  75 def try_to_serve_304(dispatch_result, request, response, etag):
  76     """Try to serve a 304 for static resources.
  77     """
  78     if not etag:
  79         # This is a request for a dynamic resource.
  80         return
  81 
  82     qs_etag = request.qs.get('etag')
  83     if qs_etag and qs_etag != etag:
  84         # Don't serve one version of a file as if it were another.
  85         raise response.error(410)
  86 
  87     headers_etag = request.headers.get(b'If-None-Match', b'').decode('ascii', 'replace')
  88     if not headers_etag:
  89         # This client doesn't want a 304.
  90         return
  91 
  92     if headers_etag != etag:
  93         # Cache miss, the client sent an old or invalid etag.
  94         return
  95 
  96     # Huzzah!
  97     # =======
  98     # We can serve a 304! :D
  99 
  100     raise response.success(304)
  101 
  102 
  103 def add_caching_to_response(response, request=None, etag=None):
  104     """Set caching headers.
  105     """
  106     if not etag:
  107         # This is a dynamic resource, disable caching by default
  108         if b'Cache-Control' not in response.headers:
  109             response.headers[b'Cache-Control'] = b'no-cache'
  110         return
  111 
  112     assert request is not None  # sanity check
  113 
  114     if response.code not in (200, 304):
  115         return
  116 
  117     # https://developers.google.com/speed/docs/best-practices/caching
  118     response.headers[b'Etag'] = etag.encode('ascii')
  119 
  120     if request.qs.get('etag'):
  121         # We can cache "indefinitely" when the querystring contains the etag.
  122         response.headers[b'Cache-Control'] = b'public, max-age=31536000'
  123     else:
  124         # Otherwise we cache for 1 hour
  125         response.headers[b'Cache-Control'] = b'public, max-age=3600'
  126
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/liberapay/utils/http_caching.py b/liberapay/utils/http_caching.py
  --- a/liberapay/utils/http_caching.py
  +++ b/liberapay/utils/http_caching.py
  @@ -79,47 +79,41 @@
           # This is a request for a dynamic resource.
           return
   
  +    # Compare the etag in the request's querystring to the one we have.
       qs_etag = request.qs.get('etag')
       if qs_etag and qs_etag != etag:
           # Don't serve one version of a file as if it were another.
           raise response.error(410)
   
  +    # Compare the etag in the request's headers to the one we have.
       headers_etag = request.headers.get(b'If-None-Match', b'').decode('ascii', 'replace')
  -    if not headers_etag:
  -        # This client doesn't want a 304.
  -        return
  -
  -    if headers_etag != etag:
  -        # Cache miss, the client sent an old or invalid etag.
  -        return
  -
  -    # Huzzah!
  -    # =======
  -    # We can serve a 304! :D
  +    if headers_etag and headers_etag == etag:
  +        # Success! We can serve a 304.
  +        raise response.success(304)
   
  -    raise response.success(304)
   
  -
  -def add_caching_to_response(response, request=None, etag=None):
  +def add_caching_to_response(state, website, response, request=None, etag=None):
       """Set caching headers.
       """
  -    if not etag:
  -        # This is a dynamic resource, disable caching by default
  -        if b'Cache-Control' not in response.headers:
  -            response.headers[b'Cache-Control'] = b'no-cache'
  -        return
  -
  -    assert request is not None  # sanity check
  -
       if response.code not in (200, 304):
           return
  -
  -    # https://developers.google.com/speed/docs/best-practices/caching
  -    response.headers[b'Etag'] = etag.encode('ascii')
  -
  -    if request.qs.get('etag'):
  -        # We can cache "indefinitely" when the querystring contains the etag.
  -        response.headers[b'Cache-Control'] = b'public, max-age=31536000'
  +    if b'Cache-Control' in response.headers:
  +        # The caching policy has already been defined somewhere else
  +        return
  +    if etag:
  +        try:
  +            assert not response.headers.cookie
  +        except Exception as e:
  +            website.tell_sentry(e, state)
  +            response.headers.cookie.clear()
  +        # https://developers.google.com/speed/docs/best-practices/caching
  +        response.headers[b'Etag'] = etag.encode('ascii')
  +        if request.qs.get('etag'):
  +            # We can cache "indefinitely" when the querystring contains the etag.
  +            response.headers[b'Cache-Control'] = b'public, max-age=31536000, immutable'
  +        else:
  +            # Otherwise we cache for 1 hour
  +            response.headers[b'Cache-Control'] = b'public, max-age=3600'
       else:
  -        # Otherwise we cache for 1 hour
  -        response.headers[b'Cache-Control'] = b'public, max-age=3600'
  +        # This is a dynamic resource, disable caching by default
  +        response.headers[b'Cache-Control'] = b'no-cache'
  
verification_info: {"golden_diff": "diff --git a/liberapay/utils/http_caching.py b/liberapay/utils/http_caching.py\n--- a/liberapay/utils/http_caching.py\n+++ b/liberapay/utils/http_caching.py\n@@ -79,47 +79,41 @@\n         # This is a request for a dynamic resource.\n         return\n \n+    # Compare the etag in the request's querystring to the one we have.\n     qs_etag = request.qs.get('etag')\n     if qs_etag and qs_etag != etag:\n         # Don't serve one version of a file as if it were another.\n         raise response.error(410)\n \n+    # Compare the etag in the request's headers to the one we have.\n     headers_etag = request.headers.get(b'If-None-Match', b'').decode('ascii', 'replace')\n-    if not headers_etag:\n-        # This client doesn't want a 304.\n-        return\n-\n-    if headers_etag != etag:\n-        # Cache miss, the client sent an old or invalid etag.\n-        return\n-\n-    # Huzzah!\n-    # =======\n-    # We can serve a 304! :D\n+    if headers_etag and headers_etag == etag:\n+        # Success! We can serve a 304.\n+        raise response.success(304)\n \n-    raise response.success(304)\n \n-\n-def add_caching_to_response(response, request=None, etag=None):\n+def add_caching_to_response(state, website, response, request=None, etag=None):\n     \"\"\"Set caching headers.\n     \"\"\"\n-    if not etag:\n-        # This is a dynamic resource, disable caching by default\n-        if b'Cache-Control' not in response.headers:\n-            response.headers[b'Cache-Control'] = b'no-cache'\n-        return\n-\n-    assert request is not None  # sanity check\n-\n     if response.code not in (200, 304):\n         return\n-\n-    # https://developers.google.com/speed/docs/best-practices/caching\n-    response.headers[b'Etag'] = etag.encode('ascii')\n-\n-    if request.qs.get('etag'):\n-        # We can cache \"indefinitely\" when the querystring contains the etag.\n-        response.headers[b'Cache-Control'] = b'public, max-age=31536000'\n+    if b'Cache-Control' in response.headers:\n+        # The caching policy has already been defined somewhere else\n+        return\n+    if etag:\n+        try:\n+            assert not response.headers.cookie\n+        except Exception as e:\n+            website.tell_sentry(e, state)\n+            response.headers.cookie.clear()\n+        # https://developers.google.com/speed/docs/best-practices/caching\n+        response.headers[b'Etag'] = etag.encode('ascii')\n+        if request.qs.get('etag'):\n+            # We can cache \"indefinitely\" when the querystring contains the etag.\n+            response.headers[b'Cache-Control'] = b'public, max-age=31536000, immutable'\n+        else:\n+            # Otherwise we cache for 1 hour\n+            response.headers[b'Cache-Control'] = b'public, max-age=3600'\n     else:\n-        # Otherwise we cache for 1 hour\n-        response.headers[b'Cache-Control'] = b'public, max-age=3600'\n+        # This is a dynamic resource, disable caching by default\n+        response.headers[b'Cache-Control'] = b'no-cache'\n", "issue": "`Cache-Control: immutable`\nIt's [a new experimental HTTP feature](https://bitsup.blogspot.fr/2016/05/cache-control-immutable.html) that we should probably start using. It's low priority though.\n\n", "before_files": [{"content": "\"\"\"\nHandles HTTP caching.\n\"\"\"\n\nimport atexit\nfrom hashlib import md5\nimport os\nfrom tempfile import mkstemp\n\nfrom aspen.request_processor.dispatcher import DispatchResult, DispatchStatus\nfrom pando import Response\n\nfrom liberapay.utils import b64encode_s, find_files\n\n\nETAGS = {}\n\n\ndef compile_assets(website):\n    cleanup = []\n    for spt in find_files(website.www_root+'/assets/', '*.spt'):\n        filepath = spt[:-4]  # /path/to/www/assets/foo.css\n        if not os.path.exists(filepath):\n            cleanup.append(filepath)\n        dispatch_result = DispatchResult(DispatchStatus.okay, spt, None, None, None)\n        state = dict(dispatch_result=dispatch_result, response=Response())\n        state['state'] = state\n        resource = website.request_processor.resources.get(spt)\n        content = resource.render(state, dispatch_result, None).body\n        if not isinstance(content, bytes):\n            content = content.encode('utf8')\n        tmpfd, tmpfpath = mkstemp(dir='.')\n        os.write(tmpfd, content)\n        os.close(tmpfd)\n        os.rename(tmpfpath, filepath)\n    if website.env.clean_assets:\n        atexit.register(lambda: rm_f(*cleanup))\n\n\ndef rm_f(*paths):\n    for path in paths:\n        try:\n            os.unlink(path)\n        except Exception:\n            pass\n\n\ndef clean_assets(www_root):\n    rm_f(*[spt[:-4] for spt in find_files(www_root+'/assets/', '*.spt')])\n\n\ndef asset_etag(path):\n    if path.endswith('.spt'):\n        return ''\n    if path in ETAGS:\n        return ETAGS[path]\n    with open(path, 'rb') as f:\n        h = b64encode_s(md5(f.read()).digest())\n    ETAGS[path] = h\n    return h\n\n\n# algorithm functions\n\ndef get_etag_for_file(dispatch_result, website, state):\n    if dispatch_result.status != DispatchStatus.okay:\n        return {'etag': None}\n    try:\n        return {'etag': asset_etag(dispatch_result.match)}\n    except Exception as e:\n        website.tell_sentry(e, state)\n        return {'etag': None}\n\n\ndef try_to_serve_304(dispatch_result, request, response, etag):\n    \"\"\"Try to serve a 304 for static resources.\n    \"\"\"\n    if not etag:\n        # This is a request for a dynamic resource.\n        return\n\n    qs_etag = request.qs.get('etag')\n    if qs_etag and qs_etag != etag:\n        # Don't serve one version of a file as if it were another.\n        raise response.error(410)\n\n    headers_etag = request.headers.get(b'If-None-Match', b'').decode('ascii', 'replace')\n    if not headers_etag:\n        # This client doesn't want a 304.\n        return\n\n    if headers_etag != etag:\n        # Cache miss, the client sent an old or invalid etag.\n        return\n\n    # Huzzah!\n    # =======\n    # We can serve a 304! :D\n\n    raise response.success(304)\n\n\ndef add_caching_to_response(response, request=None, etag=None):\n    \"\"\"Set caching headers.\n    \"\"\"\n    if not etag:\n        # This is a dynamic resource, disable caching by default\n        if b'Cache-Control' not in response.headers:\n            response.headers[b'Cache-Control'] = b'no-cache'\n        return\n\n    assert request is not None  # sanity check\n\n    if response.code not in (200, 304):\n        return\n\n    # https://developers.google.com/speed/docs/best-practices/caching\n    response.headers[b'Etag'] = etag.encode('ascii')\n\n    if request.qs.get('etag'):\n        # We can cache \"indefinitely\" when the querystring contains the etag.\n        response.headers[b'Cache-Control'] = b'public, max-age=31536000'\n    else:\n        # Otherwise we cache for 1 hour\n        response.headers[b'Cache-Control'] = b'public, max-age=3600'\n", "path": "liberapay/utils/http_caching.py"}], "after_files": [{"content": "\"\"\"\nHandles HTTP caching.\n\"\"\"\n\nimport atexit\nfrom hashlib import md5\nimport os\nfrom tempfile import mkstemp\n\nfrom aspen.request_processor.dispatcher import DispatchResult, DispatchStatus\nfrom pando import Response\n\nfrom liberapay.utils import b64encode_s, find_files\n\n\nETAGS = {}\n\n\ndef compile_assets(website):\n    cleanup = []\n    for spt in find_files(website.www_root+'/assets/', '*.spt'):\n        filepath = spt[:-4]  # /path/to/www/assets/foo.css\n        if not os.path.exists(filepath):\n            cleanup.append(filepath)\n        dispatch_result = DispatchResult(DispatchStatus.okay, spt, None, None, None)\n        state = dict(dispatch_result=dispatch_result, response=Response())\n        state['state'] = state\n        resource = website.request_processor.resources.get(spt)\n        content = resource.render(state, dispatch_result, None).body\n        if not isinstance(content, bytes):\n            content = content.encode('utf8')\n        tmpfd, tmpfpath = mkstemp(dir='.')\n        os.write(tmpfd, content)\n        os.close(tmpfd)\n        os.rename(tmpfpath, filepath)\n    if website.env.clean_assets:\n        atexit.register(lambda: rm_f(*cleanup))\n\n\ndef rm_f(*paths):\n    for path in paths:\n        try:\n            os.unlink(path)\n        except Exception:\n            pass\n\n\ndef clean_assets(www_root):\n    rm_f(*[spt[:-4] for spt in find_files(www_root+'/assets/', '*.spt')])\n\n\ndef asset_etag(path):\n    if path.endswith('.spt'):\n        return ''\n    if path in ETAGS:\n        return ETAGS[path]\n    with open(path, 'rb') as f:\n        h = b64encode_s(md5(f.read()).digest())\n    ETAGS[path] = h\n    return h\n\n\n# algorithm functions\n\ndef get_etag_for_file(dispatch_result, website, state):\n    if dispatch_result.status != DispatchStatus.okay:\n        return {'etag': None}\n    try:\n        return {'etag': asset_etag(dispatch_result.match)}\n    except Exception as e:\n        website.tell_sentry(e, state)\n        return {'etag': None}\n\n\ndef try_to_serve_304(dispatch_result, request, response, etag):\n    \"\"\"Try to serve a 304 for static resources.\n    \"\"\"\n    if not etag:\n        # This is a request for a dynamic resource.\n        return\n\n    # Compare the etag in the request's querystring to the one we have.\n    qs_etag = request.qs.get('etag')\n    if qs_etag and qs_etag != etag:\n        # Don't serve one version of a file as if it were another.\n        raise response.error(410)\n\n    # Compare the etag in the request's headers to the one we have.\n    headers_etag = request.headers.get(b'If-None-Match', b'').decode('ascii', 'replace')\n    if headers_etag and headers_etag == etag:\n        # Success! We can serve a 304.\n        raise response.success(304)\n\n\ndef add_caching_to_response(state, website, response, request=None, etag=None):\n    \"\"\"Set caching headers.\n    \"\"\"\n    if response.code not in (200, 304):\n        return\n    if b'Cache-Control' in response.headers:\n        # The caching policy has already been defined somewhere else\n        return\n    if etag:\n        try:\n            assert not response.headers.cookie\n        except Exception as e:\n            website.tell_sentry(e, state)\n            response.headers.cookie.clear()\n        # https://developers.google.com/speed/docs/best-practices/caching\n        response.headers[b'Etag'] = etag.encode('ascii')\n        if request.qs.get('etag'):\n            # We can cache \"indefinitely\" when the querystring contains the etag.\n            response.headers[b'Cache-Control'] = b'public, max-age=31536000, immutable'\n        else:\n            # Otherwise we cache for 1 hour\n            response.headers[b'Cache-Control'] = b'public, max-age=3600'\n    else:\n        # This is a dynamic resource, disable caching by default\n        response.headers[b'Cache-Control'] = b'no-cache'\n", "path": "liberapay/utils/http_caching.py"}]}