diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index c348c6c8daa..af78d1d2695 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -13,67 +13,12 @@ from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri +# For backwards compatibility -- lots of code uses this in the wild! +from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT + 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 @@ -254,11 +199,7 @@ class WSGIHandler(base.BaseHandler): 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='')))) diff --git a/django/http/response.py b/django/http/response.py index 88ac8848c21..671fb1c5734 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -20,6 +20,65 @@ from django.utils.http import cookie_date 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 @@ -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. @@ -53,9 +113,13 @@ class HttpResponseBase(six.Iterator): 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): diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index fc26eabf1a4..10c3f32e609 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -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 @@ -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. @@ -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) @@ -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: @@ -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 diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0eab8540b05..b4668c38d04 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -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 ===================================== diff --git a/tests/responses/__init__.py b/tests/responses/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/responses/models.py b/tests/responses/models.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/responses/tests.py b/tests/responses/tests.py new file mode 100644 index 00000000000..e5320f5af92 --- /dev/null +++ b/tests/responses/tests.py @@ -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)