summaryrefslogtreecommitdiff
path: root/django/http
diff options
context:
space:
mode:
authorTom Carrick <tom@carrick.eu>2020-07-14 13:32:24 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-09-14 08:41:59 +0200
commitbcc2befd0e9c1885e45b46d0b0bcdc11def8b249 (patch)
tree59fab69a3182286da87fcd6fe05a8ce0f4277a5a /django/http
parent71ae1ab0123582cc5bfe0f7d5f4cc19a9412f396 (diff)
Fixed #31789 -- Added a new headers interface to HttpResponse.
Diffstat (limited to 'django/http')
-rw-r--r--django/http/response.py128
1 files changed, 78 insertions, 50 deletions
diff --git a/django/http/response.py b/django/http/response.py
index 64ac205087..e679c856c0 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -5,6 +5,7 @@ import os
import re
import sys
import time
+from collections.abc import Mapping
from email.header import Header
from http.client import responses
from urllib.parse import quote, urlparse
@@ -15,6 +16,7 @@ from django.core.exceptions import DisallowedRedirect
from django.core.serializers.json import DjangoJSONEncoder
from django.http.cookie import SimpleCookie
from django.utils import timezone
+from django.utils.datastructures import CaseInsensitiveMapping
from django.utils.encoding import iri_to_uri
from django.utils.http import http_date
from django.utils.regex_helper import _lazy_re_compile
@@ -22,6 +24,65 @@ from django.utils.regex_helper import _lazy_re_compile
_charset_from_content_type_re = _lazy_re_compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
+class ResponseHeaders(CaseInsensitiveMapping):
+ def __init__(self, data):
+ """
+ Populate the initial data using __setitem__ to ensure values are
+ correctly encoded.
+ """
+ if not isinstance(data, Mapping):
+ data = {
+ k: v
+ for k, v in CaseInsensitiveMapping._destruct_iterable_mapping_values(data)
+ }
+ self._store = {}
+ for header, value in data.items():
+ self[header] = value
+
+ def _convert_to_charset(self, value, charset, mime_encode=False):
+ """
+ Convert headers key/value to ascii/latin-1 native strings.
+ `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
+ `value` can't be represented in the given charset, apply MIME-encoding.
+ """
+ if not isinstance(value, (bytes, str)):
+ value = str(value)
+ if (
+ (isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
+ (isinstance(value, str) and ('\n' in value or '\r' in value))
+ ):
+ raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
+ try:
+ if isinstance(value, str):
+ # Ensure string is valid in given charset
+ value.encode(charset)
+ else:
+ # Convert bytestring using given charset
+ value = value.decode(charset)
+ except UnicodeError as e:
+ if mime_encode:
+ value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
+ else:
+ e.reason += ', HTTP response headers must be in %s format' % charset
+ raise
+ return value
+
+ def __delitem__(self, key):
+ self.pop(key)
+
+ def __setitem__(self, key, value):
+ key = self._convert_to_charset(key, 'ascii')
+ value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
+ self._store[key.lower()] = (key, value)
+
+ def pop(self, key, default=None):
+ return self._store.pop(key.lower(), default)
+
+ def setdefault(self, key, value):
+ if key not in self:
+ self[key] = value
+
+
class BadHeaderError(ValueError):
pass
@@ -37,10 +98,7 @@ class HttpResponseBase:
status_code = 200
def __init__(self, content_type=None, status=None, reason=None, charset=None):
- # _headers is a mapping of the lowercase name to the original case of
- # the header (required for working with legacy systems) and the header
- # value. Both the name of the header and its value are ASCII strings.
- self._headers = {}
+ self.headers = ResponseHeaders({})
self._resource_closers = []
# This parameter is set by the handler. It's necessary to preserve the
# historical behavior of request_finished.
@@ -95,7 +153,7 @@ class HttpResponseBase:
headers = [
(to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1'))
- for key, value in self._headers.values()
+ for key, value in self.headers.items()
]
return b'\r\n'.join(headers)
@@ -103,57 +161,28 @@ class HttpResponseBase:
@property
def _content_type_for_repr(self):
- return ', "%s"' % self['Content-Type'] if 'Content-Type' in self else ''
-
- def _convert_to_charset(self, value, charset, mime_encode=False):
- """
- Convert headers key/value to ascii/latin-1 native strings.
-
- `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
- `value` can't be represented in the given charset, apply MIME-encoding.
- """
- if not isinstance(value, (bytes, str)):
- value = str(value)
- if ((isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
- isinstance(value, str) and ('\n' in value or '\r' in value)):
- raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
- try:
- if isinstance(value, str):
- # Ensure string is valid in given charset
- value.encode(charset)
- else:
- # Convert bytestring using given charset
- value = value.decode(charset)
- except UnicodeError as e:
- if mime_encode:
- value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
- else:
- e.reason += ', HTTP response headers must be in %s format' % charset
- raise
- return value
+ return ', "%s"' % self.headers['Content-Type'] if 'Content-Type' in self.headers else ''
def __setitem__(self, header, value):
- header = self._convert_to_charset(header, 'ascii')
- value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
- self._headers[header.lower()] = (header, value)
+ self.headers[header] = value
def __delitem__(self, header):
- self._headers.pop(header.lower(), False)
+ del self.headers[header]
def __getitem__(self, header):
- return self._headers[header.lower()][1]
+ return self.headers[header]
def has_header(self, header):
"""Case-insensitive check for a header."""
- return header.lower() in self._headers
+ return header in self.headers
__contains__ = has_header
def items(self):
- return self._headers.values()
+ return self.headers.items()
def get(self, header, alternate=None):
- return self._headers.get(header.lower(), (None, alternate))[1]
+ return self.headers.get(header, alternate)
def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
domain=None, secure=False, httponly=False, samesite=None):
@@ -203,8 +232,7 @@ class HttpResponseBase:
def setdefault(self, key, value):
"""Set a header unless it has already been set."""
- if key not in self:
- self[key] = value
+ self.headers.setdefault(key, value)
def set_signed_cookie(self, key, value, salt='', **kwargs):
value = signing.get_cookie_signer(salt=key + salt).sign(value)
@@ -430,19 +458,19 @@ class FileResponse(StreamingHttpResponse):
filename = getattr(filelike, 'name', None)
filename = filename if (isinstance(filename, str) and filename) else self.filename
if os.path.isabs(filename):
- self['Content-Length'] = os.path.getsize(filelike.name)
+ self.headers['Content-Length'] = os.path.getsize(filelike.name)
elif hasattr(filelike, 'getbuffer'):
- self['Content-Length'] = filelike.getbuffer().nbytes
+ self.headers['Content-Length'] = filelike.getbuffer().nbytes
- if self.get('Content-Type', '').startswith('text/html'):
+ if self.headers.get('Content-Type', '').startswith('text/html'):
if filename:
content_type, encoding = mimetypes.guess_type(filename)
# Encoding isn't set to prevent browsers from automatically
# uncompressing files.
content_type = encoding_map.get(encoding, content_type)
- self['Content-Type'] = content_type or 'application/octet-stream'
+ self.headers['Content-Type'] = content_type or 'application/octet-stream'
else:
- self['Content-Type'] = 'application/octet-stream'
+ self.headers['Content-Type'] = 'application/octet-stream'
filename = self.filename or os.path.basename(filename)
if filename:
@@ -452,9 +480,9 @@ class FileResponse(StreamingHttpResponse):
file_expr = 'filename="{}"'.format(filename)
except UnicodeEncodeError:
file_expr = "filename*=utf-8''{}".format(quote(filename))
- self['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
+ self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
elif self.as_attachment:
- self['Content-Disposition'] = 'attachment'
+ self.headers['Content-Disposition'] = 'attachment'
class HttpResponseRedirectBase(HttpResponse):