Skip to content

Commit dcb6904

Browse files
knyghtyfelixxm
authored andcommitted
Fixed #32002 -- Added headers parameter to HttpResponse and subclasses.
1 parent 2e7cc95 commit dcb6904

File tree

9 files changed

+115
-32
lines changed

9 files changed

+115
-32
lines changed

django/http/response.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,18 @@ class HttpResponseBase:
9797

9898
status_code = 200
9999

100-
def __init__(self, content_type=None, status=None, reason=None, charset=None):
101-
self.headers = ResponseHeaders({})
100+
def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None):
101+
self.headers = ResponseHeaders(headers or {})
102+
self._charset = charset
103+
if content_type and 'Content-Type' in self.headers:
104+
raise ValueError(
105+
"'headers' must not contain 'Content-Type' when the "
106+
"'content_type' parameter is provided."
107+
)
108+
if 'Content-Type' not in self.headers:
109+
if content_type is None:
110+
content_type = 'text/html; charset=%s' % self.charset
111+
self.headers['Content-Type'] = content_type
102112
self._resource_closers = []
103113
# This parameter is set by the handler. It's necessary to preserve the
104114
# historical behavior of request_finished.
@@ -114,10 +124,6 @@ def __init__(self, content_type=None, status=None, reason=None, charset=None):
114124
if not 100 <= self.status_code <= 599:
115125
raise ValueError('HTTP status code must be an integer from 100 to 599.')
116126
self._reason_phrase = reason
117-
self._charset = charset
118-
if content_type is None:
119-
content_type = 'text/html; charset=%s' % self.charset
120-
self['Content-Type'] = content_type
121127

122128
@property
123129
def reason_phrase(self):

django/template/response.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class SimpleTemplateResponse(HttpResponse):
1111
rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
1212

1313
def __init__(self, template, context=None, content_type=None, status=None,
14-
charset=None, using=None):
14+
charset=None, using=None, headers=None):
1515
# It would seem obvious to call these next two members 'template' and
1616
# 'context', but those names are reserved as part of the test Client
1717
# API. To avoid the name collision, we use different names.
@@ -33,7 +33,7 @@ def __init__(self, template, context=None, content_type=None, status=None,
3333
# content argument doesn't make sense here because it will be replaced
3434
# with rendered template so we always pass empty string in order to
3535
# prevent errors and provide shorter signature.
36-
super().__init__('', content_type, status, charset=charset)
36+
super().__init__('', content_type, status, charset=charset, headers=headers)
3737

3838
# _is_rendered tracks whether the template and context has been baked
3939
# into a final response.
@@ -139,6 +139,6 @@ class TemplateResponse(SimpleTemplateResponse):
139139
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']
140140

141141
def __init__(self, request, template, context=None, content_type=None,
142-
status=None, charset=None, using=None):
143-
super().__init__(template, context, content_type, status, charset, using)
142+
status=None, charset=None, using=None, headers=None):
143+
super().__init__(template, context, content_type, status, charset, using, headers=headers)
144144
self._request = request

docs/howto/outputting-csv.txt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ Here's an example::
2020

2121
def some_view(request):
2222
# Create the HttpResponse object with the appropriate CSV header.
23-
response = HttpResponse(content_type='text/csv')
24-
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
23+
response = HttpResponse(
24+
content_type='text/csv',
25+
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
26+
)
2527

2628
writer = csv.writer(response)
2729
writer.writerow(['First row', 'Foo', 'Bar', 'Baz'])
@@ -86,10 +88,11 @@ the assembly and transmission of a large CSV file::
8688
rows = (["Row {}".format(idx), str(idx)] for idx in range(65536))
8789
pseudo_buffer = Echo()
8890
writer = csv.writer(pseudo_buffer)
89-
response = StreamingHttpResponse((writer.writerow(row) for row in rows),
90-
content_type="text/csv")
91-
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
92-
return response
91+
return StreamingHttpResponse(
92+
(writer.writerow(row) for row in rows),
93+
content_type="text/csv",
94+
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
95+
)
9396

9497
Using the template system
9598
=========================
@@ -108,8 +111,10 @@ Here's an example, which generates the same CSV file as above::
108111

109112
def some_view(request):
110113
# Create the HttpResponse object with the appropriate CSV header.
111-
response = HttpResponse(content_type='text/csv')
112-
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
114+
response = HttpResponse(
115+
content_type='text/csv'
116+
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
117+
)
113118

114119
# The data is hard-coded here, but you could load it from a database or
115120
# some other source.

docs/ref/request-response.txt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,10 @@ by ``HttpResponse``.
724724
When using this interface, unlike a dictionary, ``del`` doesn't raise
725725
``KeyError`` if the header field doesn't exist.
726726

727+
You can also set headers on instantiation::
728+
729+
>>> response = HttpResponse(headers={'Age': 120})
730+
727731
For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended
728732
to use the :func:`~django.utils.cache.patch_cache_control` and
729733
:func:`~django.utils.cache.patch_vary_headers` methods from
@@ -738,15 +742,19 @@ containing a newline character (CR or LF) will raise ``BadHeaderError``
738742

739743
The :attr:`HttpResponse.headers` interface was added.
740744

745+
The ability to set headers on instantiation was added.
746+
741747
Telling the browser to treat the response as a file attachment
742748
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
743749

744-
To tell the browser to treat the response as a file attachment, use the
745-
``content_type`` argument and set the ``Content-Disposition`` header. For example,
746-
this is how you might return a Microsoft Excel spreadsheet::
750+
To tell the browser to treat the response as a file attachment, set the
751+
``Content-Type`` and ``Content-Disposition`` headers. For example, this is how
752+
you might return a Microsoft Excel spreadsheet::
747753

748-
>>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel')
749-
>>> response.headers['Content-Disposition'] = 'attachment; filename="foo.xls"'
754+
>>> response = HttpResponse(my_data, headers={
755+
... 'Content-Type': 'application/vnd.ms-excel',
756+
... 'Content-Disposition': 'attachment; filename="foo.xls"',
757+
... })
750758

751759
There's nothing Django-specific about the ``Content-Disposition`` header, but
752760
it's easy to forget the syntax, so we've included it here.
@@ -802,10 +810,10 @@ Attributes
802810
Methods
803811
-------
804812

805-
.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None)
813+
.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None, headers=None)
806814

807-
Instantiates an ``HttpResponse`` object with the given page content and
808-
content type.
815+
Instantiates an ``HttpResponse`` object with the given page content,
816+
content type, and headers.
809817

810818
``content`` is most commonly an iterator, bytestring, :class:`memoryview`,
811819
or string. Other types will be converted to a bytestring by encoding their
@@ -829,6 +837,12 @@ Methods
829837
given it will be extracted from ``content_type``, and if that
830838
is unsuccessful, the :setting:`DEFAULT_CHARSET` setting will be used.
831839

840+
``headers`` is a :class:`dict` of HTTP headers for the response.
841+
842+
.. versionchanged:: 3.2
843+
844+
The ``headers`` parameter was added.
845+
832846
.. method:: HttpResponse.__setitem__(header, value)
833847

834848
Sets the given header name to the given value. Both ``header`` and

docs/ref/template-response.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Attributes
5757
Methods
5858
-------
5959

60-
.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None)
60+
.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None, headers=None)
6161

6262
Instantiates a :class:`~django.template.response.SimpleTemplateResponse`
6363
object with the given template, context, content type, HTTP status, and
@@ -90,6 +90,13 @@ Methods
9090
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
9191
loading the template.
9292

93+
``headers``
94+
A :class:`dict` of HTTP headers to add to the response.
95+
96+
.. versionchanged:: 3.2
97+
98+
The ``headers`` parameter was added.
99+
93100
.. method:: SimpleTemplateResponse.resolve_context(context)
94101

95102
Preprocesses context data that will be used for rendering a template.
@@ -149,7 +156,7 @@ Methods
149156
Methods
150157
-------
151158

152-
.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None)
159+
.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None, headers=None)
153160

154161
Instantiates a :class:`~django.template.response.TemplateResponse` object
155162
with the given request, template, context, content type, HTTP status, and
@@ -185,6 +192,13 @@ Methods
185192
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
186193
loading the template.
187194

195+
``headers``
196+
A :class:`dict` of HTTP headers to add to the response.
197+
198+
.. versionchanged:: 3.2
199+
200+
The ``headers`` parameter was added.
201+
188202
The rendering process
189203
=====================
190204

docs/releases/3.2.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ Requests and Responses
332332
Both interfaces will continue to be supported. See
333333
:ref:`setting-header-fields` for details.
334334

335+
* The new ``headers`` parameter of :class:`~django.http.HttpResponse`,
336+
:class:`~django.template.response.SimpleTemplateResponse`, and
337+
:class:`~django.template.response.TemplateResponse` allows setting response
338+
:attr:`~django.http.HttpResponse.headers` on instantiation.
339+
335340
Security
336341
~~~~~~~~
337342

docs/topics/class-based-views/index.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ And the view::
117117

118118
def head(self, *args, **kwargs):
119119
last_book = self.get_queryset().latest('publication_date')
120-
response = HttpResponse()
121-
# RFC 1123 date format
122-
response.headers['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
120+
response = HttpResponse(
121+
# RFC 1123 date format.
122+
headers={'Last-Modified': last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')},
123+
)
123124
return response
124125

125126
If the view is accessed from a ``GET`` request, an object list is returned in

tests/httpwrappers/tests.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def test_fromkeys_noniterable(self):
286286
QueryDict.fromkeys(0)
287287

288288

289-
class HttpResponseTests(unittest.TestCase):
289+
class HttpResponseTests(SimpleTestCase):
290290

291291
def test_headers_type(self):
292292
r = HttpResponse()
@@ -470,10 +470,31 @@ def test_header_deletion(self):
470470
# del doesn't raise a KeyError on nonexistent headers.
471471
del r.headers['X-Foo']
472472

473+
def test_instantiate_with_headers(self):
474+
r = HttpResponse('hello', headers={'X-Foo': 'foo'})
475+
self.assertEqual(r.headers['X-Foo'], 'foo')
476+
self.assertEqual(r.headers['x-foo'], 'foo')
477+
473478
def test_content_type(self):
474479
r = HttpResponse('hello', content_type='application/json')
475480
self.assertEqual(r.headers['Content-Type'], 'application/json')
476481

482+
def test_content_type_headers(self):
483+
r = HttpResponse('hello', headers={'Content-Type': 'application/json'})
484+
self.assertEqual(r.headers['Content-Type'], 'application/json')
485+
486+
def test_content_type_mutually_exclusive(self):
487+
msg = (
488+
"'headers' must not contain 'Content-Type' when the "
489+
"'content_type' parameter is provided."
490+
)
491+
with self.assertRaisesMessage(ValueError, msg):
492+
HttpResponse(
493+
'hello',
494+
content_type='application/json',
495+
headers={'Content-Type': 'text/csv'},
496+
)
497+
477498

478499
class HttpResponseSubclassesTests(SimpleTestCase):
479500
def test_redirect(self):

tests/template_tests/test_response.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ def test_pickling_cookie(self):
216216

217217
self.assertEqual(unpickled_response.cookies['key'].value, 'value')
218218

219+
def test_headers(self):
220+
response = SimpleTemplateResponse(
221+
'first/test.html',
222+
{'value': 123, 'fn': datetime.now},
223+
headers={'X-Foo': 'foo'},
224+
)
225+
self.assertEqual(response.headers['X-Foo'], 'foo')
226+
219227

220228
@override_settings(TEMPLATES=[{
221229
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -319,6 +327,15 @@ def test_repickling(self):
319327
unpickled_response = pickle.loads(pickled_response)
320328
pickle.dumps(unpickled_response)
321329

330+
def test_headers(self):
331+
response = TemplateResponse(
332+
self.factory.get('/'),
333+
'first/test.html',
334+
{'value': 123, 'fn': datetime.now},
335+
headers={'X-Foo': 'foo'},
336+
)
337+
self.assertEqual(response.headers['X-Foo'], 'foo')
338+
322339

323340
@modify_settings(MIDDLEWARE={'append': ['template_tests.test_response.custom_urlconf_middleware']})
324341
@override_settings(ROOT_URLCONF='template_tests.urls')

0 commit comments

Comments
 (0)