Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #12747 -- Made reason phrases customizable. #1154

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 1 addition & 63 deletions django/core/handlers/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,64 +16,6 @@
logger = logging.getLogger('django.request')


# See http://www.iana.org/assignments/http-status-codes
STATUS_CODE_TEXT = {
100: 'CONTINUE',
101: 'SWITCHING PROTOCOLS',
102: 'PROCESSING',
200: 'OK',
201: 'CREATED',
202: 'ACCEPTED',
203: 'NON-AUTHORITATIVE INFORMATION',
204: 'NO CONTENT',
205: 'RESET CONTENT',
206: 'PARTIAL CONTENT',
207: 'MULTI-STATUS',
208: 'ALREADY REPORTED',
226: 'IM USED',
300: 'MULTIPLE CHOICES',
301: 'MOVED PERMANENTLY',
302: 'FOUND',
303: 'SEE OTHER',
304: 'NOT MODIFIED',
305: 'USE PROXY',
306: 'RESERVED',
307: 'TEMPORARY REDIRECT',
400: 'BAD REQUEST',
401: 'UNAUTHORIZED',
402: 'PAYMENT REQUIRED',
403: 'FORBIDDEN',
404: 'NOT FOUND',
405: 'METHOD NOT ALLOWED',
406: 'NOT ACCEPTABLE',
407: 'PROXY AUTHENTICATION REQUIRED',
408: 'REQUEST TIMEOUT',
409: 'CONFLICT',
410: 'GONE',
411: 'LENGTH REQUIRED',
412: 'PRECONDITION FAILED',
413: 'REQUEST ENTITY TOO LARGE',
414: 'REQUEST-URI TOO LONG',
415: 'UNSUPPORTED MEDIA TYPE',
416: 'REQUESTED RANGE NOT SATISFIABLE',
417: 'EXPECTATION FAILED',
418: "I'M A TEAPOT",
422: 'UNPROCESSABLE ENTITY',
423: 'LOCKED',
424: 'FAILED DEPENDENCY',
426: 'UPGRADE REQUIRED',
500: 'INTERNAL SERVER ERROR',
501: 'NOT IMPLEMENTED',
502: 'BAD GATEWAY',
503: 'SERVICE UNAVAILABLE',
504: 'GATEWAY TIMEOUT',
505: 'HTTP VERSION NOT SUPPORTED',
506: 'VARIANT ALSO NEGOTIATES',
507: 'INSUFFICIENT STORAGE',
508: 'LOOP DETECTED',
510: 'NOT EXTENDED',
}

class LimitedStream(object):
'''
LimitedStream wraps another stream in order to not allow reading from it
Expand Down Expand Up @@ -254,11 +196,7 @@ def __call__(self, environ, start_response):

response._handler_class = self.__class__

try:
status_text = STATUS_CODE_TEXT[response.status_code]
except KeyError:
status_text = 'UNKNOWN STATUS CODE'
status = '%s %s' % (response.status_code, status_text)
status = '%s %s' % (response.status_code, response.reason_phrase)
response_headers = [(str(k), str(v)) for k, v in response.items()]
for c in response.cookies.values():
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
Expand Down
70 changes: 67 additions & 3 deletions django/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,65 @@
from django.utils.six.moves import map


# See http://www.iana.org/assignments/http-status-codes
REASON_PHRASES = {
100: 'CONTINUE',
101: 'SWITCHING PROTOCOLS',
102: 'PROCESSING',
200: 'OK',
201: 'CREATED',
202: 'ACCEPTED',
203: 'NON-AUTHORITATIVE INFORMATION',
204: 'NO CONTENT',
205: 'RESET CONTENT',
206: 'PARTIAL CONTENT',
207: 'MULTI-STATUS',
208: 'ALREADY REPORTED',
226: 'IM USED',
300: 'MULTIPLE CHOICES',
301: 'MOVED PERMANENTLY',
302: 'FOUND',
303: 'SEE OTHER',
304: 'NOT MODIFIED',
305: 'USE PROXY',
306: 'RESERVED',
307: 'TEMPORARY REDIRECT',
400: 'BAD REQUEST',
401: 'UNAUTHORIZED',
402: 'PAYMENT REQUIRED',
403: 'FORBIDDEN',
404: 'NOT FOUND',
405: 'METHOD NOT ALLOWED',
406: 'NOT ACCEPTABLE',
407: 'PROXY AUTHENTICATION REQUIRED',
408: 'REQUEST TIMEOUT',
409: 'CONFLICT',
410: 'GONE',
411: 'LENGTH REQUIRED',
412: 'PRECONDITION FAILED',
413: 'REQUEST ENTITY TOO LARGE',
414: 'REQUEST-URI TOO LONG',
415: 'UNSUPPORTED MEDIA TYPE',
416: 'REQUESTED RANGE NOT SATISFIABLE',
417: 'EXPECTATION FAILED',
418: "I'M A TEAPOT",
422: 'UNPROCESSABLE ENTITY',
423: 'LOCKED',
424: 'FAILED DEPENDENCY',
426: 'UPGRADE REQUIRED',
500: 'INTERNAL SERVER ERROR',
501: 'NOT IMPLEMENTED',
502: 'BAD GATEWAY',
503: 'SERVICE UNAVAILABLE',
504: 'GATEWAY TIMEOUT',
505: 'HTTP VERSION NOT SUPPORTED',
506: 'VARIANT ALSO NEGOTIATES',
507: 'INSUFFICIENT STORAGE',
508: 'LOOP DETECTED',
510: 'NOT EXTENDED',
}


class BadHeaderError(ValueError):
pass

Expand All @@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator):
"""

status_code = 200
reason_phrase = None # Use default reason phrase for status code.

def __init__(self, content_type=None, status=None, mimetype=None):
def __init__(self, content_type=None, status=None, reason=None, mimetype=None):
# _headers is a mapping of the lower-case 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.
Expand All @@ -53,9 +113,13 @@ def __init__(self, content_type=None, status=None, mimetype=None):
content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
self._charset)
self.cookies = SimpleCookie()
if status:
if status is not None:
self.status_code = status

if reason is not None:
self.reason_phrase = reason
elif self.reason_phrase is None:
self.reason_phrase = REASON_PHRASES.get(self.status_code,
'UNKNOWN STATUS CODE')
self['Content-Type'] = content_type

def serialize_headers(self):
Expand Down
27 changes: 21 additions & 6 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,13 @@ Attributes

.. attribute:: HttpResponse.status_code

The `HTTP Status code`_ for the response.
The `HTTP status code`_ for the response.

.. attribute:: HttpResponse.reason_phrase

.. versionadded:: 1.6

The HTTP reason phrase for the response.

.. attribute:: HttpResponse.streaming

Expand All @@ -628,7 +634,7 @@ Attributes
Methods
-------

.. method:: HttpResponse.__init__(content='', content_type=None, status=200)
.. method:: HttpResponse.__init__(content='', content_type=None, status=200, reason=None)

Instantiates an ``HttpResponse`` object with the given page content and
content type.
Expand All @@ -646,8 +652,12 @@ Methods

Historically, this parameter was called ``mimetype`` (now deprecated).

``status`` is the `HTTP Status code`_ for the response.
``status`` is the `HTTP status code`_ for the response.

.. versionadded:: 1.6

``reason`` is the HTTP response phrase. If not provided, a default phrase
will be used.

.. method:: HttpResponse.__setitem__(header, value)

Expand Down Expand Up @@ -727,8 +737,7 @@ Methods

This method makes an :class:`HttpResponse` instance a file-like object.

.. _HTTP Status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10

.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10

.. _ref-httpresponse-subclasses:

Expand Down Expand Up @@ -851,7 +860,13 @@ Attributes

.. attribute:: HttpResponse.status_code

The `HTTP Status code`_ for the response.
The `HTTP status code`_ for the response.

.. attribute:: HttpResponse.reason_phrase

.. versionadded:: 1.6

The HTTP reason phrase for the response.

.. attribute:: HttpResponse.streaming

Expand Down
2 changes: 2 additions & 0 deletions docs/releases/1.6.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ Minor features
* The ``choices`` argument to model fields now accepts an iterable of iterables
instead of requiring an iterable of lists or tuples.

* The reason phrase can be customized in HTTP responses.

Backwards incompatible changes in 1.6
=====================================

Expand Down
Empty file added tests/responses/__init__.py
Empty file.
Empty file added tests/responses/models.py
Empty file.
15 changes: 15 additions & 0 deletions tests/responses/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.http import HttpResponse
import unittest

class HttpResponseTests(unittest.TestCase):

def test_status_code(self):
resp = HttpResponse(status=418)
self.assertEqual(resp.status_code, 418)
self.assertEqual(resp.reason_phrase, "I'M A TEAPOT")

def test_reason_phrase(self):
reason = "I'm an anarchist coffee pot on crack."
resp = HttpResponse(status=814, reason=reason)
self.assertEqual(resp.status_code, 814)
self.assertEqual(resp.reason_phrase, reason)