Skip to content

Commit 51e56b8

Browse files
committed
Add COMPRESS_EVALUATE_CONDITIONAL_REQUEST new behaviour
1 parent 758a56b commit 51e56b8

File tree

3 files changed

+151
-4
lines changed

3 files changed

+151
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,4 @@ Within your Flask application's settings you can provide the following settings
144144
| `COMPRESS_REGISTER` | Specifies if compression should be automatically registered. | `True` |
145145
| `COMPRESS_ALGORITHM` | Supported compression algorithms. | `['zstd', 'br', 'gzip', 'deflate']` |
146146
| `COMPRESS_STREAMS` | Compress content streams. | `True` |
147+
| `COMPRESS_EVALUATE_CONDITIONAL_REQUEST` | Compress evaluates conditional requests. | `False` |

flask_compress/flask_compress.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def init_app(self, app):
165165
("COMPRESS_CACHE_BACKEND", None),
166166
("COMPRESS_REGISTER", True),
167167
("COMPRESS_STREAMS", True),
168+
("COMPRESS_EVALUATE_CONDITIONAL_REQUEST", False),
168169
("COMPRESS_ALGORITHM", ["zstd", "br", "gzip", "deflate"]),
169170
]
170171

@@ -230,9 +231,17 @@ def after_request(self, response):
230231

231232
# "123456789" => "123456789:gzip" - A strong ETag validator
232233
# W/"123456789" => W/"123456789:gzip" - A weak ETag validator
233-
etag = response.headers.get("ETag")
234-
if etag:
235-
response.headers["ETag"] = f'{etag[:-1]}:{chosen_algorithm}"'
234+
etag, is_weak = response.get_etag()
235+
236+
if etag and not is_weak:
237+
response.set_etag(f"{etag}:{chosen_algorithm}", weak=is_weak)
238+
239+
if (
240+
app.config["COMPRESS_EVALUATE_CONDITIONAL_REQUEST"]
241+
and request.method in ("GET", "HEAD")
242+
and (not response.is_streamed or app.config["COMPRESS_STREAMS"])
243+
):
244+
response.make_conditional(request)
236245

237246
return response
238247

tests/test_flask_compress.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44
import unittest
55

6-
from flask import Flask, render_template
6+
from flask import Flask, make_response, render_template, request
77
from flask_caching import Cache
88

99
from flask_compress import Compress, DictCache
@@ -91,6 +91,13 @@ def test_quality_level_default_zstd(self):
9191
"""Tests COMPRESS_ZSTD_LEVEL default value is correctly set."""
9292
self.assertEqual(self.app.config["COMPRESS_ZSTD_LEVEL"], 3)
9393

94+
def test_evaluate_conditional_request(self):
95+
"""Tests COMPRESS_EVALUATE_CONDITIONAL_REQUEST default value
96+
is correctly set."""
97+
self.assertEqual(
98+
self.app.config["COMPRESS_EVALUATE_CONDITIONAL_REQUEST"], False
99+
)
100+
94101

95102
class InitTests(unittest.TestCase):
96103
def setUp(self):
@@ -570,5 +577,135 @@ def test_compression(self):
570577
self.assertEqual(self.cache_key_calls, 2)
571578

572579

580+
class ETagTests(unittest.TestCase):
581+
def setUp(self):
582+
self.app = Flask(__name__)
583+
self.app.testing = True
584+
self.app.config["COMPRESS_ALGORITHM"] = ["gzip"]
585+
self.app.config["COMPRESS_MIN_SIZE"] = 1
586+
587+
Compress(self.app)
588+
589+
@self.app.route("/strong/")
590+
def strong():
591+
rv = make_response(render_template("large.html"))
592+
rv.set_etag("abc123", weak=False)
593+
return rv.make_conditional(request)
594+
595+
@self.app.route("/strong-compress-conditional/")
596+
def strong_compress_conditional():
597+
rv = make_response(render_template("large.html"))
598+
rv.set_etag("abc123", weak=False)
599+
return rv
600+
601+
@self.app.route("/weak/")
602+
def weak():
603+
rv = make_response(render_template("large.html"))
604+
rv.set_etag("abc123", weak=True)
605+
return rv.make_conditional(request)
606+
607+
@self.app.route("/weak-compress-conditional/")
608+
def weak_compress_conditional():
609+
rv = make_response(render_template("large.html"))
610+
rv.set_etag("abc123", weak=True)
611+
return rv
612+
613+
def test_strong_etag_is_mutated_with_suffix_and_remains_strong(self):
614+
client = self.app.test_client()
615+
r = client.get("/strong/", headers=[("Accept-Encoding", "gzip")])
616+
self.assertEqual(r.status_code, 200)
617+
self.assertEqual(r.headers.get("Content-Encoding"), "gzip")
618+
619+
tag, is_weak = r.get_etag()
620+
self.assertFalse(is_weak)
621+
self.assertEqual(tag, "abc123:gzip")
622+
self.assertEqual(int(r.headers["Content-Length"]), len(r.data))
623+
624+
def test_weak_etag_is_preserved(self):
625+
client = self.app.test_client()
626+
r = client.get("/weak/", headers=[("Accept-Encoding", "gzip")])
627+
self.assertEqual(r.status_code, 200)
628+
self.assertEqual(r.headers.get("Content-Encoding"), "gzip")
629+
630+
tag, is_weak = r.get_etag()
631+
self.assertTrue(is_weak)
632+
# No :gzip suffix when flag is False
633+
self.assertEqual(tag, "abc123")
634+
635+
def test_conditional_get_uses_strong_compressed_representation(self):
636+
client = self.app.test_client()
637+
r1 = client.get("/strong/", headers=[("Accept-Encoding", "gzip")])
638+
639+
r2 = client.get(
640+
"/strong/",
641+
headers=[
642+
("Accept-Encoding", "gzip"),
643+
("If-None-Match", r1.headers["ETag"]),
644+
],
645+
)
646+
# This is the current behavior that breaks make_conditional
647+
# strong etags due rewrite at after_request
648+
# We would expect a 304 but it does not because of etag mismatch
649+
self.assertEqual(r2.status_code, 200)
650+
651+
def test_conditional_get_uses_weak_compressed_representation(self):
652+
client = self.app.test_client()
653+
r1 = client.get("/weak/", headers=[("Accept-Encoding", "gzip")])
654+
etag_header = r1.headers["ETag"]
655+
656+
r2 = client.get(
657+
"/weak/",
658+
headers=[("Accept-Encoding", "gzip"), ("If-None-Match", etag_header)],
659+
)
660+
# This is the new behaviour we would expect by not mutating
661+
# the weak etags at after_request
662+
self.assertEqual(r2.status_code, 304)
663+
self.assertEqual(r2.headers.get("ETag"), etag_header)
664+
self.assertNotIn("Content-Encoding", r2.headers)
665+
self.assertEqual(len(r2.get_data()), 0)
666+
667+
def test_conditional_get_uses_strong_compressed_representation_evaluate_conditional(
668+
self,
669+
):
670+
self.app.config["COMPRESS_EVALUATE_CONDITIONAL_REQUEST"] = True
671+
client = self.app.test_client()
672+
r1 = client.get(
673+
"/strong-compress-conditional/", headers=[("Accept-Encoding", "gzip")]
674+
)
675+
etag_header = r1.headers["ETag"]
676+
677+
r2 = client.get(
678+
"/strong-compress-conditional/",
679+
headers=[("Accept-Encoding", "gzip"), ("If-None-Match", etag_header)],
680+
)
681+
# This is the new behaviour we would expect after evaluating
682+
# flask make_conditional at after_request
683+
self.assertEqual(r2.status_code, 304)
684+
self.assertEqual(r2.headers.get("ETag"), etag_header)
685+
self.assertNotIn("Content-Encoding", r2.headers)
686+
self.assertEqual(len(r2.get_data()), 0)
687+
688+
def test_conditional_get_uses_weak_compressed_representation_evaluate_conditional(
689+
self,
690+
):
691+
self.app.config["COMPRESS_EVALUATE_CONDITIONAL_REQUEST"] = True
692+
client = self.app.test_client()
693+
r1 = client.get(
694+
"/weak-compress-conditional/", headers=[("Accept-Encoding", "gzip")]
695+
)
696+
etag_header = r1.headers["ETag"]
697+
698+
r2 = client.get(
699+
"/weak-compress-conditional/",
700+
headers=[("Accept-Encoding", "gzip"), ("If-None-Match", etag_header)],
701+
)
702+
# This is the new behaviour we would expect after evaluating
703+
# flask make_conditional at after_request
704+
self.assertEqual(r2.status_code, 304)
705+
self.assertEqual(r2.headers.get("ETag"), etag_header)
706+
self.assertNotIn("Content-Encoding", r2.headers)
707+
self.assertEqual(len(r2.get_data()), 0)
708+
709+
573710
if __name__ == "__main__":
574711
unittest.main()

0 commit comments

Comments
 (0)