mirror of
https://github.com/django/django.git
synced 2025-08-08 12:58:37 +00:00

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.
Backport of c075508b4d
from main.
279 lines
10 KiB
Python
279 lines
10 KiB
Python
from unittest import TestCase
|
|
|
|
from django.http import HttpRequest
|
|
from django.http.request import MediaType
|
|
|
|
|
|
class MediaTypeTests(TestCase):
|
|
def test_empty(self):
|
|
for empty_media_type in (None, "", " "):
|
|
with self.subTest(media_type=empty_media_type):
|
|
media_type = MediaType(empty_media_type)
|
|
self.assertEqual(str(media_type), "")
|
|
self.assertEqual(repr(media_type), "<MediaType: >")
|
|
|
|
def test_str(self):
|
|
self.assertEqual(str(MediaType("*/*; q=0.8")), "*/*; q=0.8")
|
|
self.assertEqual(str(MediaType("application/xml")), "application/xml")
|
|
|
|
def test_repr(self):
|
|
self.assertEqual(repr(MediaType("*/*; q=0.8")), "<MediaType: */*; q=0.8>")
|
|
self.assertEqual(
|
|
repr(MediaType("application/xml")),
|
|
"<MediaType: application/xml>",
|
|
)
|
|
|
|
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):
|
|
self.assertIs(MediaType(accepted_type).match(mime_type), True)
|
|
|
|
def test_no_match(self):
|
|
tests = [
|
|
# 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"),
|
|
("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):
|
|
self.assertIs(MediaType(accepted_type).match(mime_type), False)
|
|
|
|
def test_quality(self):
|
|
tests = [
|
|
("*/*; q=0.8", 0.8),
|
|
("*/*; q=0.0001", 0),
|
|
("*/*; q=0.12345", 0.123),
|
|
("*/*; q=0.1", 0.1),
|
|
("*/*; q=-1", 1),
|
|
("*/*; q=2", 1),
|
|
("*/*; q=h", 1),
|
|
("*/*; q=inf", 1),
|
|
("*/*; q=0", 0),
|
|
("*/*", 1),
|
|
]
|
|
for accepted_type, quality in tests:
|
|
with self.subTest(accepted_type, quality=quality):
|
|
self.assertEqual(MediaType(accepted_type).quality, quality)
|
|
|
|
def test_specificity(self):
|
|
tests = [
|
|
("*/*", 0),
|
|
("*/*;q=0.5", 0),
|
|
("text/*", 1),
|
|
("text/*;q=0.5", 1),
|
|
("text/html", 2),
|
|
("text/html;q=1", 2),
|
|
("text/html;q=0.5", 2),
|
|
("text/html;version=5", 3),
|
|
]
|
|
for accepted_type, specificity in tests:
|
|
with self.subTest(accepted_type, specificity=specificity):
|
|
self.assertEqual(MediaType(accepted_type).specificity, specificity)
|
|
|
|
|
|
class AcceptHeaderTests(TestCase):
|
|
def test_no_headers(self):
|
|
"""Absence of Accept header defaults to '*/*'."""
|
|
request = HttpRequest()
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
["*/*"],
|
|
)
|
|
|
|
def test_accept_headers(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/*,text/html, application/xhtml+xml,application/xml ;q=0.9,*/*;q=0.8,"
|
|
)
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
[
|
|
"text/html",
|
|
"application/xhtml+xml",
|
|
"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"] = "*/*"
|
|
self.assertIs(request.accepts("application/json"), True)
|
|
self.assertIsNone(request.get_preferred_type([]))
|
|
self.assertEqual(
|
|
request.get_preferred_type(["application/json", "text/plain"]),
|
|
"application/json",
|
|
)
|
|
|
|
def test_request_accepts_none(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = ""
|
|
self.assertIs(request.accepts("application/json"), False)
|
|
self.assertEqual(request.accepted_types, [])
|
|
self.assertIsNone(
|
|
request.get_preferred_type(["application/json", "text/plain"])
|
|
)
|
|
|
|
def test_request_accepts_some(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9"
|
|
)
|
|
self.assertIs(request.accepts("text/html"), True)
|
|
self.assertIs(request.accepts("application/xhtml+xml"), True)
|
|
self.assertIs(request.accepts("application/xml"), True)
|
|
self.assertIs(request.accepts("application/json"), False)
|
|
|
|
def test_accept_header_priority(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/html,application/xml;q=0.9,*/*;q=0.1,text/*;q=0.5"
|
|
)
|
|
|
|
tests = [
|
|
(["text/html", "application/xml"], "text/html"),
|
|
(["application/xml", "application/json"], "application/xml"),
|
|
(["application/json"], "application/json"),
|
|
(["application/json", "text/plain"], "text/plain"),
|
|
]
|
|
for types, preferred_type in tests:
|
|
with self.subTest(types, preferred_type=preferred_type):
|
|
self.assertEqual(str(request.get_preferred_type(types)), preferred_type)
|
|
|
|
def test_accept_header_priority_overlapping_mime(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/*;q=0.8,text/html;q=0.8"
|
|
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
[
|
|
"text/html; q=0.8",
|
|
"text/*; q=0.8",
|
|
],
|
|
)
|
|
|
|
def test_no_matching_accepted_type(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/html"
|
|
|
|
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"
|
|
)
|