mirror of
https://github.com/django/django.git
synced 2025-10-24 22:26:08 +00:00
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.
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user