Fixed #12747 -- Made reason phrases customizable.

This commit is contained in:
Aymeric Augustin 2013-05-19 12:58:13 +02:00
parent 3129d19071
commit cb86f707a0
7 changed files with 109 additions and 72 deletions

View File

@ -13,67 +13,12 @@ from django.core.urlresolvers import set_script_prefix
from django.utils import datastructures from django.utils import datastructures
from django.utils.encoding import force_str, force_text, iri_to_uri 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') 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): class LimitedStream(object):
''' '''
LimitedStream wraps another stream in order to not allow reading from it 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__ response._handler_class = self.__class__
try: status = '%s %s' % (response.status_code, response.reason_phrase)
status_text = STATUS_CODE_TEXT[response.status_code]
except KeyError:
status_text = 'UNKNOWN STATUS CODE'
status = '%s %s' % (response.status_code, status_text)
response_headers = [(str(k), str(v)) for k, v in response.items()] response_headers = [(str(k), str(v)) for k, v in response.items()]
for c in response.cookies.values(): for c in response.cookies.values():
response_headers.append((str('Set-Cookie'), str(c.output(header='')))) response_headers.append((str('Set-Cookie'), str(c.output(header=''))))

View File

@ -20,6 +20,65 @@ from django.utils.http import cookie_date
from django.utils.six.moves import map 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): class BadHeaderError(ValueError):
pass pass
@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator):
""" """
status_code = 200 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 # _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 # the header (required for working with legacy systems) and the header
# value. Both the name of the header and its value are ASCII strings. # 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, content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
self._charset) self._charset)
self.cookies = SimpleCookie() self.cookies = SimpleCookie()
if status: if status is not None:
self.status_code = status 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 self['Content-Type'] = content_type
def serialize_headers(self): def serialize_headers(self):

View File

@ -616,7 +616,13 @@ Attributes
.. attribute:: HttpResponse.status_code .. 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 .. attribute:: HttpResponse.streaming
@ -628,7 +634,7 @@ Attributes
Methods 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 Instantiates an ``HttpResponse`` object with the given page content and
content type. content type.
@ -646,8 +652,12 @@ Methods
Historically, this parameter was called ``mimetype`` (now deprecated). 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) .. method:: HttpResponse.__setitem__(header, value)
@ -727,8 +737,7 @@ Methods
This method makes an :class:`HttpResponse` instance a file-like object. 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: .. _ref-httpresponse-subclasses:
@ -851,7 +860,13 @@ Attributes
.. attribute:: HttpResponse.status_code .. 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 .. attribute:: HttpResponse.streaming

View File

@ -241,6 +241,8 @@ Minor features
* The ``choices`` argument to model fields now accepts an iterable of iterables * The ``choices`` argument to model fields now accepts an iterable of iterables
instead of requiring an iterable of lists or tuples. instead of requiring an iterable of lists or tuples.
* The reason phrase can be customized in HTTP responses.
Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================

View File

View File

15
tests/responses/tests.py Normal file
View File

@ -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)