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 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=''))))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -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