Fixed #12747 -- Made reason phrases customizable.
This commit is contained in:
parent
3129d19071
commit
cb86f707a0
|
@ -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=''))))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
=====================================
|
||||
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue