diff --git a/django/http/response.py b/django/http/response.py index e679c856c0..eedc03f118 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -97,8 +97,18 @@ class HttpResponseBase: status_code = 200 - def __init__(self, content_type=None, status=None, reason=None, charset=None): - self.headers = ResponseHeaders({}) + def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None): + self.headers = ResponseHeaders(headers or {}) + self._charset = charset + if content_type and 'Content-Type' in self.headers: + raise ValueError( + "'headers' must not contain 'Content-Type' when the " + "'content_type' parameter is provided." + ) + if 'Content-Type' not in self.headers: + if content_type is None: + content_type = 'text/html; charset=%s' % self.charset + self.headers['Content-Type'] = content_type self._resource_closers = [] # This parameter is set by the handler. It's necessary to preserve the # historical behavior of request_finished. @@ -114,10 +124,6 @@ class HttpResponseBase: if not 100 <= self.status_code <= 599: raise ValueError('HTTP status code must be an integer from 100 to 599.') self._reason_phrase = reason - self._charset = charset - if content_type is None: - content_type = 'text/html; charset=%s' % self.charset - self['Content-Type'] = content_type @property def reason_phrase(self): diff --git a/django/template/response.py b/django/template/response.py index 30b35b611c..9efadcd726 100644 --- a/django/template/response.py +++ b/django/template/response.py @@ -11,7 +11,7 @@ class SimpleTemplateResponse(HttpResponse): rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks'] def __init__(self, template, context=None, content_type=None, status=None, - charset=None, using=None): + charset=None, using=None, headers=None): # It would seem obvious to call these next two members 'template' and # 'context', but those names are reserved as part of the test Client # API. To avoid the name collision, we use different names. @@ -33,7 +33,7 @@ class SimpleTemplateResponse(HttpResponse): # content argument doesn't make sense here because it will be replaced # with rendered template so we always pass empty string in order to # prevent errors and provide shorter signature. - super().__init__('', content_type, status, charset=charset) + super().__init__('', content_type, status, charset=charset, headers=headers) # _is_rendered tracks whether the template and context has been baked # into a final response. @@ -139,6 +139,6 @@ class TemplateResponse(SimpleTemplateResponse): rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request'] def __init__(self, request, template, context=None, content_type=None, - status=None, charset=None, using=None): - super().__init__(template, context, content_type, status, charset, using) + status=None, charset=None, using=None, headers=None): + super().__init__(template, context, content_type, status, charset, using, headers=headers) self._request = request diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index dc3cf57cfd..0026d0293a 100644 --- a/docs/howto/outputting-csv.txt +++ b/docs/howto/outputting-csv.txt @@ -20,8 +20,10 @@ Here's an example:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. - response = HttpResponse(content_type='text/csv') - response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"' + response = HttpResponse( + content_type='text/csv', + headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'}, + ) writer = csv.writer(response) writer.writerow(['First row', 'Foo', 'Bar', 'Baz']) @@ -86,10 +88,11 @@ the assembly and transmission of a large CSV file:: rows = (["Row {}".format(idx), str(idx)] for idx in range(65536)) pseudo_buffer = Echo() writer = csv.writer(pseudo_buffer) - response = StreamingHttpResponse((writer.writerow(row) for row in rows), - content_type="text/csv") - response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"' - return response + return StreamingHttpResponse( + (writer.writerow(row) for row in rows), + content_type="text/csv", + headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'}, + ) Using the template system ========================= @@ -108,8 +111,10 @@ Here's an example, which generates the same CSV file as above:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. - response = HttpResponse(content_type='text/csv') - response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"' + response = HttpResponse( + content_type='text/csv' + headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'}, + ) # The data is hard-coded here, but you could load it from a database or # some other source. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 8dae4455a2..17aebf2317 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -724,6 +724,10 @@ by ``HttpResponse``. When using this interface, unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header field doesn't exist. +You can also set headers on instantiation:: + + >>> response = HttpResponse(headers={'Age': 120}) + For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended to use the :func:`~django.utils.cache.patch_cache_control` and :func:`~django.utils.cache.patch_vary_headers` methods from @@ -738,15 +742,19 @@ containing a newline character (CR or LF) will raise ``BadHeaderError`` The :attr:`HttpResponse.headers` interface was added. + The ability to set headers on instantiation was added. + Telling the browser to treat the response as a file attachment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To tell the browser to treat the response as a file attachment, use the -``content_type`` argument and set the ``Content-Disposition`` header. For example, -this is how you might return a Microsoft Excel spreadsheet:: +To tell the browser to treat the response as a file attachment, set the +``Content-Type`` and ``Content-Disposition`` headers. For example, this is how +you might return a Microsoft Excel spreadsheet:: - >>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel') - >>> response.headers['Content-Disposition'] = 'attachment; filename="foo.xls"' + >>> response = HttpResponse(my_data, headers={ + ... 'Content-Type': 'application/vnd.ms-excel', + ... 'Content-Disposition': 'attachment; filename="foo.xls"', + ... }) There's nothing Django-specific about the ``Content-Disposition`` header, but it's easy to forget the syntax, so we've included it here. @@ -802,10 +810,10 @@ Attributes Methods ------- -.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None) +.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None, headers=None) - Instantiates an ``HttpResponse`` object with the given page content and - content type. + Instantiates an ``HttpResponse`` object with the given page content, + content type, and headers. ``content`` is most commonly an iterator, bytestring, :class:`memoryview`, or string. Other types will be converted to a bytestring by encoding their @@ -829,6 +837,12 @@ Methods given it will be extracted from ``content_type``, and if that is unsuccessful, the :setting:`DEFAULT_CHARSET` setting will be used. + ``headers`` is a :class:`dict` of HTTP headers for the response. + + .. versionchanged:: 3.2 + + The ``headers`` parameter was added. + .. method:: HttpResponse.__setitem__(header, value) Sets the given header name to the given value. Both ``header`` and diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index decf055660..52802f5bd1 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -57,7 +57,7 @@ Attributes Methods ------- -.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None) +.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None, headers=None) Instantiates a :class:`~django.template.response.SimpleTemplateResponse` object with the given template, context, content type, HTTP status, and @@ -90,6 +90,13 @@ Methods The :setting:`NAME ` of a template engine to use for loading the template. + ``headers`` + A :class:`dict` of HTTP headers to add to the response. + + .. versionchanged:: 3.2 + + The ``headers`` parameter was added. + .. method:: SimpleTemplateResponse.resolve_context(context) Preprocesses context data that will be used for rendering a template. @@ -149,7 +156,7 @@ Methods Methods ------- -.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None) +.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None, headers=None) Instantiates a :class:`~django.template.response.TemplateResponse` object with the given request, template, context, content type, HTTP status, and @@ -185,6 +192,13 @@ Methods The :setting:`NAME ` of a template engine to use for loading the template. + ``headers`` + A :class:`dict` of HTTP headers to add to the response. + + .. versionchanged:: 3.2 + + The ``headers`` parameter was added. + The rendering process ===================== diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 4987423ff3..d82c9c719a 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -332,6 +332,11 @@ Requests and Responses Both interfaces will continue to be supported. See :ref:`setting-header-fields` for details. +* The new ``headers`` parameter of :class:`~django.http.HttpResponse`, + :class:`~django.template.response.SimpleTemplateResponse`, and + :class:`~django.template.response.TemplateResponse` allows setting response + :attr:`~django.http.HttpResponse.headers` on instantiation. + Security ~~~~~~~~ diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 8874545469..01f9c35460 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -117,9 +117,10 @@ And the view:: def head(self, *args, **kwargs): last_book = self.get_queryset().latest('publication_date') - response = HttpResponse() - # RFC 1123 date format - response.headers['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + response = HttpResponse( + # RFC 1123 date format. + headers={'Last-Modified': last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')}, + ) return response If the view is accessed from a ``GET`` request, an object list is returned in diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index fbc5019979..cbf8b7d586 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -286,7 +286,7 @@ class QueryDictTests(SimpleTestCase): QueryDict.fromkeys(0) -class HttpResponseTests(unittest.TestCase): +class HttpResponseTests(SimpleTestCase): def test_headers_type(self): r = HttpResponse() @@ -470,10 +470,31 @@ class HttpResponseTests(unittest.TestCase): # del doesn't raise a KeyError on nonexistent headers. del r.headers['X-Foo'] + def test_instantiate_with_headers(self): + r = HttpResponse('hello', headers={'X-Foo': 'foo'}) + self.assertEqual(r.headers['X-Foo'], 'foo') + self.assertEqual(r.headers['x-foo'], 'foo') + def test_content_type(self): r = HttpResponse('hello', content_type='application/json') self.assertEqual(r.headers['Content-Type'], 'application/json') + def test_content_type_headers(self): + r = HttpResponse('hello', headers={'Content-Type': 'application/json'}) + self.assertEqual(r.headers['Content-Type'], 'application/json') + + def test_content_type_mutually_exclusive(self): + msg = ( + "'headers' must not contain 'Content-Type' when the " + "'content_type' parameter is provided." + ) + with self.assertRaisesMessage(ValueError, msg): + HttpResponse( + 'hello', + content_type='application/json', + headers={'Content-Type': 'text/csv'}, + ) + class HttpResponseSubclassesTests(SimpleTestCase): def test_redirect(self): diff --git a/tests/template_tests/test_response.py b/tests/template_tests/test_response.py index 0a51d68f01..8ac629f3bd 100644 --- a/tests/template_tests/test_response.py +++ b/tests/template_tests/test_response.py @@ -216,6 +216,14 @@ class SimpleTemplateResponseTest(SimpleTestCase): self.assertEqual(unpickled_response.cookies['key'].value, 'value') + def test_headers(self): + response = SimpleTemplateResponse( + 'first/test.html', + {'value': 123, 'fn': datetime.now}, + headers={'X-Foo': 'foo'}, + ) + self.assertEqual(response.headers['X-Foo'], 'foo') + @override_settings(TEMPLATES=[{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -319,6 +327,15 @@ class TemplateResponseTest(SimpleTestCase): unpickled_response = pickle.loads(pickled_response) pickle.dumps(unpickled_response) + def test_headers(self): + response = TemplateResponse( + self.factory.get('/'), + 'first/test.html', + {'value': 123, 'fn': datetime.now}, + headers={'X-Foo': 'foo'}, + ) + self.assertEqual(response.headers['X-Foo'], 'foo') + @modify_settings(MIDDLEWARE={'append': ['template_tests.test_response.custom_urlconf_middleware']}) @override_settings(ROOT_URLCONF='template_tests.urls')