diff --git a/sanic/response/convenience.py b/sanic/response/convenience.py index 429b3214a4..d8a3059773 100644 --- a/sanic/response/convenience.py +++ b/sanic/response/convenience.py @@ -148,7 +148,26 @@ async def validate_file( last_modified = datetime.fromtimestamp( float(last_modified), tz=timezone.utc ).replace(microsecond=0) - if last_modified <= if_modified_since: + + if ( + last_modified.utcoffset() is None + and if_modified_since.utcoffset() is not None + ): + logger.warning( + "Cannot compare tz-aware and tz-naive datetimes. To avoid " + "this conflict Sanic is converting last_modified to UTC." + ) + last_modified.replace(tzinfo=timezone.utc) + elif ( + last_modified.utcoffset() is not None + and if_modified_since.utcoffset() is None + ): + logger.warning( + "Cannot compare tz-aware and tz-naive datetimes. To avoid " + "this conflict Sanic is converting if_modified_since to UTC." + ) + if_modified_since.replace(tzinfo=timezone.utc) + if last_modified.timestamp() <= if_modified_since.timestamp(): return HTTPResponse(status=304) diff --git a/tests/test_response_file.py b/tests/test_response_file.py new file mode 100644 index 0000000000..366e613e8d --- /dev/null +++ b/tests/test_response_file.py @@ -0,0 +1,55 @@ +from datetime import datetime, timezone +from logging import INFO + +import pytest + +from sanic.compat import Header +from sanic.response.convenience import validate_file + + +@pytest.mark.parametrize( + "ifmod,lastmod,expected", + ( + ("Sat, 01 Apr 2023 00:00:00 GMT", 1672524000, None), + ( + "Sat, 01 Apr 2023 00:00:00", + 1672524000, + "converting if_modified_since", + ), + ( + "Sat, 01 Apr 2023 00:00:00 GMT", + datetime(2023, 1, 1, 0, 0, 0), + "converting last_modified", + ), + ( + "Sat, 01 Apr 2023 00:00:00", + datetime(2023, 1, 1, 0, 0, 0), + None, + ), + ( + "Sat, 01 Apr 2023 00:00:00 GMT", + datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc), + None, + ), + ( + "Sat, 01 Apr 2023 00:00:00", + datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc), + "converting if_modified_since", + ), + ), +) +@pytest.mark.asyncio +async def test_file_timestamp_validation( + lastmod, ifmod, expected, caplog: pytest.LogCaptureFixture +): + headers = Header([["If-Modified-Since", ifmod]]) + + with caplog.at_level(INFO): + response = await validate_file(headers, lastmod) + assert response.status == 304 + records = caplog.records + if not expected: + assert len(records) == 0 + else: + record = records[0] + assert expected in record.message