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 /django/http/request.py | |
| 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 'django/http/request.py')
| -rw-r--r-- | django/http/request.py | 61 |
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 |
