{"action":"opened","issue":{"url":"https://api.github.com/repos/bottlepy/bottle/issues/718","labels_url":"https://api.github.com/repos/bottlepy/bottle/issues/718/labels{/name}","comments_url":"https://api.github.com/repos/bottlepy/bottle/issues/718/comments","events_url":"https://api.github.com/repos/bottlepy/bottle/issues/718/events","html_url":"https://github.com/bottlepy/bottle/issues/718","id":57824258,"number":718,"title":"static_file behaves incorrectly when serving different files from the same URL, due to caching","user":{"login":"AardvarkSoup","id":1272852,"avatar_url":"https://avatars.githubusercontent.com/u/1272852?v=3","gravatar_id":"","url":"https://api.github.com/users/AardvarkSoup","html_url":"https://github.com/AardvarkSoup","followers_url":"https://api.github.com/users/AardvarkSoup/followers","following_url":"https://api.github.com/users/AardvarkSoup/following{/other_user}","gists_url":"https://api.github.com/users/AardvarkSoup/gists{/gist_id}","starred_url":"https://api.github.com/users/AardvarkSoup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AardvarkSoup/subscriptions","organizations_url":"https://api.github.com/users/AardvarkSoup/orgs","repos_url":"https://api.github.com/users/AardvarkSoup/repos","events_url":"https://api.github.com/users/AardvarkSoup/events{/privacy}","received_events_url":"https://api.github.com/users/AardvarkSoup/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-02-16T16:26:34Z","updated_at":"2015-02-16T16:26:34Z","closed_at":null,"body":"I recently encountered a strange issue where my browser was returned a `304 Not Modified` response by my Bottle app, even though the content it was fetching most defininately was modified. It turns out this was caused by a problem in `static_file`:\r\n\r\n### The problem\r\n\r\nGiven a router that looks like this:\r\n\r\n    @get('/some-url')\r\n    def someFile():\r\n        if some_dynamic_property:\r\n            return static_file('some-file-A')\r\n        else:\r\n            return static_file('some-file-B')\r\n\r\nGiven that:\r\n\r\n- The modification time of `some-file-A` (as provided by the filesystem) is greater than that of `some-file-B`.\r\n- `some_dynamic_property` is `True`.\r\n\r\nThen, the following situation can occur:\r\n\r\n- A client fetches `/some-url`.\r\n- A 200 response arrives with the contents of `some-file-A` and its `Last-Modified` header set to that file's modification date.\r\n- `some_dynamic_property` is changed to `False` (meaning future requests should yield `some-file-B`).\r\n- The same client requests `/some-url` again, with the `If-Modified-Since` header set to the previously received\r\n  `Last-Modified` value.\r\n- **A 304 Not Modified response is returned**, leading the client to continue using `some-file-A`.\r\n\r\n### Expected behavior\r\n\r\nInstead of a 304 response, a 200 response with the contents of `some-file-B` should have been returned.\r\n\r\n\r\n### Cause\r\n\r\nIn order to determine whether to return a `304 Not Modified` response, `static_file` compares the `If-Modified-Since` header of the request to the modification date of the file to serve. However, it incorrectly assumes this modification date actually corresponds to the same file; when this is not the case (i.e. whenever one route may serve more than a single different file), it incorrectly tells the client it can reuse the previous result.\r\n\r\n\r\n### Quick workaround\r\n\r\nUnset the `Last-Modified` header:\r\n\r\n    @get('/some-url')\r\n    def someFile():\r\n        if some_dynamic_property:\r\n            resp = static_file('some-file-A')\r\n        else:\r\n            resp = static_file('some-file-B')\r\n\r\n        resp.set_header('Last-Modified', '')\r\n        return resp\r\n\r\n\r\nNote that **this disables client-side caching for this URL**, which is often undesirable. Otherwise, one has to manually implement one of the solutions listed below.\r\n\r\n\r\n### Potential solutions\r\n\r\n- Stop using the `Last-Modified` header for caching but instead provide an `ETag`, derived from a checksum/hash of the file contents.\r\n- A disadvantage of the previous approach is that the file always has to be read in its entirety before a hash can be calculated. This can be prevented by caching hashes until the corresponding files change, or by obtaining the tag by hashing a file's path along with its modification date.\r\n- Add an argument to `static_file` that allows a caching method to be selected. The options could be `'none'`,\r\n  `'etag'`, `'last-modified'` and `'etag+last-modified'`. Preferably `'last-modified'` should not be the default.\r\n- Declare this to be intended behavior, but explicitly mention this issue in the documentation of `static_file`.\r\n\r\n\r\nI hope that this problem can be resolved. If desired, I could provide a patch implementing one of these solutions myself.\r\n"}}