diff options
| author | Jake Howard <git@theorangeone.net> | 2025-05-27 17:00:29 +0100 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-06-03 16:10:41 -0300 |
| commit | c075508b4de8edf9db553b409f8a8ed2f26ecead (patch) | |
| tree | 49ed74a80f9af3be8bf116ad8eb30b8856a4c199 /tests | |
| parent | 26313bc21932d0d3af278ab387549d63b1f64575 (diff) | |
Fixed #36411 -- Made HttpRequest.get_preferred_type() consider media type parameters.
HttpRequest.get_preferred_type() did not account for parameters in
Accept header media types (e.g., "text/vcard; version=3.0"). This caused
incorrect content negotiation when multiple types differed only by
parameters, reducing specificity as per RFC 7231 section 5.3.2
(https://datatracker.ietf.org/doc/html/rfc7231.html#section-5.3.2).
This fix updates get_preferred_type() to treat media types with
parameters as distinct, allowing more precise and standards-compliant
matching.
Thanks to magicfelix for the report, and to David Sanders and Sarah
Boyce for the reviews.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/requests_tests/test_accept_header.py | 130 |
1 files changed, 116 insertions, 14 deletions
diff --git a/tests/requests_tests/test_accept_header.py b/tests/requests_tests/test_accept_header.py index 6585fec678..37826278e2 100644 --- a/tests/requests_tests/test_accept_header.py +++ b/tests/requests_tests/test_accept_header.py @@ -6,10 +6,9 @@ from django.http.request import MediaType class MediaTypeTests(TestCase): def test_empty(self): - for empty_media_type in (None, ""): + for empty_media_type in (None, "", " "): with self.subTest(media_type=empty_media_type): media_type = MediaType(empty_media_type) - self.assertIs(media_type.is_all_types, False) self.assertEqual(str(media_type), "") self.assertEqual(repr(media_type), "<MediaType: >") @@ -24,21 +23,18 @@ class MediaTypeTests(TestCase): "<MediaType: application/xml>", ) - def test_is_all_types(self): - self.assertIs(MediaType("*/*").is_all_types, True) - self.assertIs(MediaType("*/*; q=0.8").is_all_types, True) - self.assertIs(MediaType("text/*").is_all_types, False) - self.assertIs(MediaType("application/xml").is_all_types, False) - def test_match(self): tests = [ ("*/*; q=0.8", "*/*"), ("*/*", "application/json"), (" */* ", "application/json"), ("application/*", "application/json"), + ("application/*", "application/*"), ("application/xml", "application/xml"), (" application/xml ", "application/xml"), ("application/xml", " application/xml "), + ("text/vcard; version=4.0", "text/vcard; version=4.0"), + ("text/vcard; version=4.0", "text/vcard"), ] for accepted_type, mime_type in tests: with self.subTest(accepted_type, mime_type=mime_type): @@ -46,11 +42,23 @@ class MediaTypeTests(TestCase): def test_no_match(self): tests = [ - (None, "*/*"), - ("", "*/*"), - ("; q=0.8", "*/*"), + # other is falsey. + ("*/*", None), + ("*/*", ""), + # other is malformed. + ("*/*", "; q=0.8"), + # main_type is falsey. + ("/*", "*/*"), + # other.main_type is falsey. + ("*/*", "/*"), + # main sub_type is falsey. + ("application", "application/*"), + # other.sub_type is falsey. + ("application/*", "application"), + # All main and sub types are defined, but there is no match. ("application/xml", "application/html"), - ("application/xml", "*/*"), + ("text/vcard; version=4.0", "text/vcard; version=3.0"), + ("text/vcard", "text/vcard; version=3.0"), ] for accepted_type, mime_type in tests: with self.subTest(accepted_type, mime_type=mime_type): @@ -65,6 +73,8 @@ class MediaTypeTests(TestCase): ("*/*; q=-1", 1), ("*/*; q=2", 1), ("*/*; q=h", 1), + ("*/*; q=inf", 1), + ("*/*; q=0", 0), ("*/*", 1), ] for accepted_type, quality in tests: @@ -79,7 +89,8 @@ class MediaTypeTests(TestCase): ("text/*;q=0.5", 1), ("text/html", 2), ("text/html;q=1", 2), - ("text/html;q=0.5", 3), + ("text/html;q=0.5", 2), + ("text/html;version=5", 3), ] for accepted_type, specificity in tests: with self.subTest(accepted_type, specificity=specificity): @@ -105,12 +116,38 @@ class AcceptHeaderTests(TestCase): [ "text/html", "application/xhtml+xml", - "text/*", "application/xml; q=0.9", + "text/*", "*/*; q=0.8", ], ) + def test_zero_quality(self): + request = HttpRequest() + request.META["HTTP_ACCEPT"] = "text/*;q=0,text/html" + self.assertEqual( + [str(accepted_type) for accepted_type in request.accepted_types], + ["text/html"], + ) + + def test_precedence(self): + """ + Taken from https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2. + """ + request = HttpRequest() + request.META["HTTP_ACCEPT"] = ( + "text/*, text/plain, text/plain;format=flowed, */*" + ) + self.assertEqual( + [str(accepted_type) for accepted_type in request.accepted_types], + [ + "text/plain; format=flowed", + "text/plain", + "text/*", + "*/*", + ], + ) + def test_request_accepts_any(self): request = HttpRequest() request.META["HTTP_ACCEPT"] = "*/*" @@ -175,3 +212,68 @@ class AcceptHeaderTests(TestCase): self.assertIsNone( request.get_preferred_type(["application/json", "text/plain"]) ) + + def test_accept_with_param(self): + request = HttpRequest() + request.META["HTTP_ACCEPT"] = "text/vcard; version=3.0, text/html;q=0.5" + + for media_types, expected in [ + ( + [ + "text/vcard; version=4.0", + "text/vcard; version=3.0", + "text/vcard", + "text/directory", + ], + "text/vcard; version=3.0", + ), + (["text/vcard; version=4.0", "text/vcard", "text/directory"], None), + (["text/vcard; version=4.0", "text/html"], "text/html"), + ]: + self.assertEqual(request.get_preferred_type(media_types), expected) + + def test_quality(self): + """ + Taken from https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2. + """ + request = HttpRequest() + request.META["HTTP_ACCEPT"] = ( + "text/*;q=0.3,text/html;q=0.7,text/html;level=1,text/html;level=2;q=0.4," + "*/*;q=0.5" + ) + + for media_type, quality in [ + ("text/html;level=1", 1), + ("text/html", 0.7), + ("text/plain", 0.3), + ("image/jpeg", 0.5), + ("text/html;level=2", 0.4), + ("text/html;level=3", 0.7), + ]: + with self.subTest(media_type): + accepted_media_type = request.accepted_type(media_type) + self.assertIsNotNone(accepted_media_type) + self.assertEqual(accepted_media_type.quality, quality) + + for media_types, expected in [ + (["text/html", "text/html; level=1"], "text/html; level=1"), + (["text/html; level=2", "text/html; level=3"], "text/html; level=2"), + ]: + self.assertEqual(request.get_preferred_type(media_types), expected) + + def test_quality_breaks_specificity(self): + """ + With the same specificity, the quality breaks the tie. + """ + request = HttpRequest() + request.META["HTTP_ACCEPT"] = "text/plain;q=0.5,text/html" + + self.assertEqual(request.accepted_type("text/plain").quality, 0.5) + self.assertEqual(request.accepted_type("text/plain").specificity, 2) + + self.assertEqual(request.accepted_type("text/html").quality, 1) + self.assertEqual(request.accepted_type("text/html").specificity, 2) + + self.assertEqual( + request.get_preferred_type(["text/html", "text/plain"]), "text/html" + ) |
