You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Originally posted by markus1978 October 6, 2023
tl;tr: It seems that starlette.staticfiles.StaticFiles does not use "proper" HTTP etags which causes issues with reverse proxies and browsers.
The starlette.staticfiles.StaticFiles class supports etags, e.g. it creates a Etag header in all responses and considers If-None-Match headers in its is_not_modified method. However, it only puts the plain hash values in the response Etag header and also only interprets If-None-Match values as plain hash values . HTTP Etags are either weak or strong and the values are supposed to be wrapped in W/"..." or "..." respectively (https://datatracker.ietf.org/doc/html/rfc7232#section-2.3.1).
This causes two problems for me. First, nginx as a reverse proxy is considering all starlette etag headers as weak etags and the gzip module removes. Secondly, when the browser sends a strong or weak etag in the If-None-Match header, it never matches because '...some-tag...' != '"...some-tag..."'.
I currently use this work arround to solve my issues:
classStaticFiles(StarletteStaticFiles):
etag_re=r'^(W/)?"?([^"]*)"?$'defis_not_modified(
self, response_headers: Headers, request_headers: Headers
) ->bool:
try:
if_none_match=request_headers["if-none-match"]
# I remove all weak/strong etag syntax, basically doing a weak match match=re.match(StaticFiles.etag_re, if_none_match)
if_none_match=match.group(2)
etag=response_headers["etag"]
ifif_none_match==etag:
returnTrueexceptKeyError:
passreturnsuper().is_not_modified(response_headers, request_headers)
deffile_response(
self, full_path: PathLike, stat_result: os.stat_result, scope: Scope,
status_code: int=200,
) ->Response:
response=super().file_response(full_path, stat_result, scope, status_code)
# I add strong etag syntax, basically wrapping the etag value in "..."if'etag'inresponse.headers:
response.headers['etag'] =f'"{response.headers.get("etag")}"'returnresponse
I am not an expert in etags, just trying to make it work in our app. I am glad for any hints, if i am getting it wrong or missing anything here.
For reference, the relevant part from the current starlette.
From staticfiles.py::StaticFiles
defis_not_modified(
self, response_headers: Headers, request_headers: Headers
) ->bool:
""" Given the request and response headers, return `True` if an HTTP "Not Modified" response could be returned instead. """try:
if_none_match=request_headers["if-none-match"]
etag=response_headers["etag"]
ifif_none_match==etag:
returnTrueexceptKeyError:
passtry:
if_modified_since=parsedate(request_headers["if-modified-since"])
last_modified=parsedate(response_headers["last-modified"])
if (
if_modified_sinceisnotNoneandlast_modifiedisnotNoneandif_modified_since>=last_modified
):
returnTrueexceptKeyError:
passreturnFalse
It seems the ETag header was incorrectly generated.
It should be a double-quoted string.
That's why I guess some implementations in the middle of the client and the Starlette application can entirely remove ETag.
Also, when the If-None-Match header is used, the server should use weak comparison, so W/"1" should match "1", but Starlette does not respect that rule, so removing the W/ prefix is also necessary.
Discussed in #2297
Originally posted by markus1978 October 6, 2023
tl;tr: It seems that
starlette.staticfiles.StaticFiles
does not use "proper" HTTP etags which causes issues with reverse proxies and browsers.The
starlette.staticfiles.StaticFiles
class supports etags, e.g. it creates aEtag
header in all responses and considersIf-None-Match
headers in itsis_not_modified
method. However, it only puts the plain hash values in the responseEtag
header and also only interpretsIf-None-Match
values as plain hash values . HTTP Etags are either weak or strong and the values are supposed to be wrapped inW/"..."
or"..."
respectively (https://datatracker.ietf.org/doc/html/rfc7232#section-2.3.1).This causes two problems for me. First, nginx as a reverse proxy is considering all starlette etag headers as weak etags and the gzip module removes. Secondly, when the browser sends a strong or weak etag in the
If-None-Match
header, it never matches because'...some-tag...' != '"...some-tag..."'
.I currently use this work arround to solve my issues:
I am not an expert in etags, just trying to make it work in our app. I am glad for any hints, if i am getting it wrong or missing anything here.
For reference, the relevant part from the current starlette.
From
staticfiles.py::StaticFiles
From
responses.py::FileResponse
The text was updated successfully, but these errors were encountered: