summaryrefslogtreecommitdiff
path: root/django/http/request.py
diff options
context:
space:
mode:
authorJake Howard <git@theorangeone.net>2025-05-27 17:00:29 +0100
committernessita <124304+nessita@users.noreply.github.com>2025-06-03 16:10:41 -0300
commitc075508b4de8edf9db553b409f8a8ed2f26ecead (patch)
tree49ed74a80f9af3be8bf116ad8eb30b8856a4c199 /django/http/request.py
parent26313bc21932d0d3af278ab387549d63b1f64575 (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 'django/http/request.py')
-rw-r--r--django/http/request.py61
1 files changed, 44 insertions, 17 deletions
diff --git a/django/http/request.py b/django/http/request.py
index 986d4eee89..6438f26268 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -93,8 +93,12 @@ class HttpRequest:
"""Return a list of MediaType instances, in order of preference."""
header_value = self.headers.get("Accept", "*/*")
return sorted(
- (MediaType(token) for token in header_value.split(",") if token.strip()),
- key=operator.attrgetter("quality", "specificity"),
+ (
+ media_type
+ for token in header_value.split(",")
+ if token.strip() and (media_type := MediaType(token)).quality != 0
+ ),
+ key=operator.attrgetter("specificity", "quality"),
reverse=True,
)
@@ -102,11 +106,12 @@ class HttpRequest:
"""
Return the preferred MediaType instance which matches the given media type.
"""
+ media_type = MediaType(media_type)
return next(
(
accepted_type
for accepted_type in self.accepted_types
- if accepted_type.match(media_type)
+ if media_type.match(accepted_type)
),
None,
)
@@ -689,13 +694,13 @@ class QueryDict(MultiValueDict):
class MediaType:
def __init__(self, media_type_raw_line):
- full_type, self.params = parse_header_parameters(
+ full_type, self._params = parse_header_parameters(
media_type_raw_line if media_type_raw_line else ""
)
self.main_type, _, self.sub_type = full_type.partition("/")
def __str__(self):
- params_str = "".join("; %s=%s" % (k, v) for k, v in self.params.items())
+ params_str = "".join("; %s=%s" % (k, v) for k, v in self._params.items())
return "%s%s%s" % (
self.main_type,
("/%s" % self.sub_type) if self.sub_type else "",
@@ -705,23 +710,45 @@ class MediaType:
def __repr__(self):
return "<%s: %s>" % (self.__class__.__qualname__, self)
- @property
- def is_all_types(self):
- return self.main_type == "*" and self.sub_type == "*"
+ @cached_property
+ def params(self):
+ params = self._params.copy()
+ params.pop("q", None)
+ return params
def match(self, other):
- if self.is_all_types:
- return True
- other = MediaType(other)
- return self.main_type == other.main_type and self.sub_type in {
- "*",
- other.sub_type,
- }
+ if not other:
+ return False
+
+ if not isinstance(other, MediaType):
+ other = MediaType(other)
+
+ main_types = [self.main_type, other.main_type]
+ sub_types = [self.sub_type, other.sub_type]
+
+ # Main types and sub types must be defined.
+ if not all((*main_types, *sub_types)):
+ return False
+
+ # Main types must match or one be "*", same for sub types.
+ for this_type, other_type in (main_types, sub_types):
+ if this_type != other_type and this_type != "*" and other_type != "*":
+ return False
+
+ if bool(self.params) == bool(other.params):
+ # If both have params or neither have params, they must be identical.
+ result = self.params == other.params
+ else:
+ # If self has params and other does not, it's a match.
+ # If other has params and self does not, don't match.
+ result = bool(self.params or not other.params)
+
+ return result
@cached_property
def quality(self):
try:
- quality = float(self.params.get("q", 1))
+ quality = float(self._params.get("q", 1))
except ValueError:
# Discard invalid values.
return 1
@@ -741,7 +768,7 @@ class MediaType:
return 0
elif self.sub_type == "*":
return 1
- elif self.quality == 1:
+ elif not self.params:
return 2
return 3