Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #12747 -- Made reason phrases customizable.

  • Loading branch information...
commit cb86f707a04e5635817d5f37a1443f9bf7d6af21 1 parent 3129d19
Aymeric Augustin authored May 19, 2013
67  django/core/handlers/wsgi.py
@@ -13,66 +13,11 @@
13 13
 from django.utils import datastructures
14 14
 from django.utils.encoding import force_str, force_text, iri_to_uri
15 15
 
16  
-logger = logging.getLogger('django.request')
  16
+# For backwards compatibility -- lots of code uses this in the wild!
  17
+from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT
17 18
 
  19
+logger = logging.getLogger('django.request')
18 20
 
19  
-# See http://www.iana.org/assignments/http-status-codes
20  
-STATUS_CODE_TEXT = {
21  
-    100: 'CONTINUE',
22  
-    101: 'SWITCHING PROTOCOLS',
23  
-    102: 'PROCESSING',
24  
-    200: 'OK',
25  
-    201: 'CREATED',
26  
-    202: 'ACCEPTED',
27  
-    203: 'NON-AUTHORITATIVE INFORMATION',
28  
-    204: 'NO CONTENT',
29  
-    205: 'RESET CONTENT',
30  
-    206: 'PARTIAL CONTENT',
31  
-    207: 'MULTI-STATUS',
32  
-    208: 'ALREADY REPORTED',
33  
-    226: 'IM USED',
34  
-    300: 'MULTIPLE CHOICES',
35  
-    301: 'MOVED PERMANENTLY',
36  
-    302: 'FOUND',
37  
-    303: 'SEE OTHER',
38  
-    304: 'NOT MODIFIED',
39  
-    305: 'USE PROXY',
40  
-    306: 'RESERVED',
41  
-    307: 'TEMPORARY REDIRECT',
42  
-    400: 'BAD REQUEST',
43  
-    401: 'UNAUTHORIZED',
44  
-    402: 'PAYMENT REQUIRED',
45  
-    403: 'FORBIDDEN',
46  
-    404: 'NOT FOUND',
47  
-    405: 'METHOD NOT ALLOWED',
48  
-    406: 'NOT ACCEPTABLE',
49  
-    407: 'PROXY AUTHENTICATION REQUIRED',
50  
-    408: 'REQUEST TIMEOUT',
51  
-    409: 'CONFLICT',
52  
-    410: 'GONE',
53  
-    411: 'LENGTH REQUIRED',
54  
-    412: 'PRECONDITION FAILED',
55  
-    413: 'REQUEST ENTITY TOO LARGE',
56  
-    414: 'REQUEST-URI TOO LONG',
57  
-    415: 'UNSUPPORTED MEDIA TYPE',
58  
-    416: 'REQUESTED RANGE NOT SATISFIABLE',
59  
-    417: 'EXPECTATION FAILED',
60  
-    418: "I'M A TEAPOT",
61  
-    422: 'UNPROCESSABLE ENTITY',
62  
-    423: 'LOCKED',
63  
-    424: 'FAILED DEPENDENCY',
64  
-    426: 'UPGRADE REQUIRED',
65  
-    500: 'INTERNAL SERVER ERROR',
66  
-    501: 'NOT IMPLEMENTED',
67  
-    502: 'BAD GATEWAY',
68  
-    503: 'SERVICE UNAVAILABLE',
69  
-    504: 'GATEWAY TIMEOUT',
70  
-    505: 'HTTP VERSION NOT SUPPORTED',
71  
-    506: 'VARIANT ALSO NEGOTIATES',
72  
-    507: 'INSUFFICIENT STORAGE',
73  
-    508: 'LOOP DETECTED',
74  
-    510: 'NOT EXTENDED',
75  
-}
76 21
 
77 22
 class LimitedStream(object):
78 23
     '''
@@ -254,11 +199,7 @@ def __call__(self, environ, start_response):
254 199
 
255 200
         response._handler_class = self.__class__
256 201
 
257  
-        try:
258  
-            status_text = STATUS_CODE_TEXT[response.status_code]
259  
-        except KeyError:
260  
-            status_text = 'UNKNOWN STATUS CODE'
261  
-        status = '%s %s' % (response.status_code, status_text)
  202
+        status = '%s %s' % (response.status_code, response.reason_phrase)
262 203
         response_headers = [(str(k), str(v)) for k, v in response.items()]
263 204
         for c in response.cookies.values():
264 205
             response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
70  django/http/response.py
@@ -20,6 +20,65 @@
20 20
 from django.utils.six.moves import map
21 21
 
22 22
 
  23
+# See http://www.iana.org/assignments/http-status-codes
  24
+REASON_PHRASES = {
  25
+    100: 'CONTINUE',
  26
+    101: 'SWITCHING PROTOCOLS',
  27
+    102: 'PROCESSING',
  28
+    200: 'OK',
  29
+    201: 'CREATED',
  30
+    202: 'ACCEPTED',
  31
+    203: 'NON-AUTHORITATIVE INFORMATION',
  32
+    204: 'NO CONTENT',
  33
+    205: 'RESET CONTENT',
  34
+    206: 'PARTIAL CONTENT',
  35
+    207: 'MULTI-STATUS',
  36
+    208: 'ALREADY REPORTED',
  37
+    226: 'IM USED',
  38
+    300: 'MULTIPLE CHOICES',
  39
+    301: 'MOVED PERMANENTLY',
  40
+    302: 'FOUND',
  41
+    303: 'SEE OTHER',
  42
+    304: 'NOT MODIFIED',
  43
+    305: 'USE PROXY',
  44
+    306: 'RESERVED',
  45
+    307: 'TEMPORARY REDIRECT',
  46
+    400: 'BAD REQUEST',
  47
+    401: 'UNAUTHORIZED',
  48
+    402: 'PAYMENT REQUIRED',
  49
+    403: 'FORBIDDEN',
  50
+    404: 'NOT FOUND',
  51
+    405: 'METHOD NOT ALLOWED',
  52
+    406: 'NOT ACCEPTABLE',
  53
+    407: 'PROXY AUTHENTICATION REQUIRED',
  54
+    408: 'REQUEST TIMEOUT',
  55
+    409: 'CONFLICT',
  56
+    410: 'GONE',
  57
+    411: 'LENGTH REQUIRED',
  58
+    412: 'PRECONDITION FAILED',
  59
+    413: 'REQUEST ENTITY TOO LARGE',
  60
+    414: 'REQUEST-URI TOO LONG',
  61
+    415: 'UNSUPPORTED MEDIA TYPE',
  62
+    416: 'REQUESTED RANGE NOT SATISFIABLE',
  63
+    417: 'EXPECTATION FAILED',
  64
+    418: "I'M A TEAPOT",
  65
+    422: 'UNPROCESSABLE ENTITY',
  66
+    423: 'LOCKED',
  67
+    424: 'FAILED DEPENDENCY',
  68
+    426: 'UPGRADE REQUIRED',
  69
+    500: 'INTERNAL SERVER ERROR',
  70
+    501: 'NOT IMPLEMENTED',
  71
+    502: 'BAD GATEWAY',
  72
+    503: 'SERVICE UNAVAILABLE',
  73
+    504: 'GATEWAY TIMEOUT',
  74
+    505: 'HTTP VERSION NOT SUPPORTED',
  75
+    506: 'VARIANT ALSO NEGOTIATES',
  76
+    507: 'INSUFFICIENT STORAGE',
  77
+    508: 'LOOP DETECTED',
  78
+    510: 'NOT EXTENDED',
  79
+}
  80
+
  81
+
23 82
 class BadHeaderError(ValueError):
24 83
     pass
25 84
 
@@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator):
33 92
     """
34 93
 
35 94
     status_code = 200
  95
+    reason_phrase = None        # Use default reason phrase for status code.
36 96
 
37  
-    def __init__(self, content_type=None, status=None, mimetype=None):
  97
+    def __init__(self, content_type=None, status=None, reason=None, mimetype=None):
38 98
         # _headers is a mapping of the lower-case name to the original case of
39 99
         # the header (required for working with legacy systems) and the header
40 100
         # value. Both the name of the header and its value are ASCII strings.
@@ -53,9 +113,13 @@ def __init__(self, content_type=None, status=None, mimetype=None):
53 113
             content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
54 114
                     self._charset)
55 115
         self.cookies = SimpleCookie()
56  
-        if status:
  116
+        if status is not None:
57 117
             self.status_code = status
58  
-
  118
+        if reason is not None:
  119
+            self.reason_phrase = reason
  120
+        elif self.reason_phrase is None:
  121
+            self.reason_phrase = REASON_PHRASES.get(self.status_code,
  122
+                                                    'UNKNOWN STATUS CODE')
59 123
         self['Content-Type'] = content_type
60 124
 
61 125
     def serialize_headers(self):
27  docs/ref/request-response.txt
@@ -616,7 +616,13 @@ Attributes
616 616
 
617 617
 .. attribute:: HttpResponse.status_code
618 618
 
619  
-    The `HTTP Status code`_ for the response.
  619
+    The `HTTP status code`_ for the response.
  620
+
  621
+.. attribute:: HttpResponse.reason_phrase
  622
+
  623
+    .. versionadded:: 1.6
  624
+
  625
+    The HTTP reason phrase for the response.
620 626
 
621 627
 .. attribute:: HttpResponse.streaming
622 628
 
@@ -628,7 +634,7 @@ Attributes
628 634
 Methods
629 635
 -------
630 636
 
631  
-.. method:: HttpResponse.__init__(content='', content_type=None, status=200)
  637
+.. method:: HttpResponse.__init__(content='', content_type=None, status=200, reason=None)
632 638
 
633 639
     Instantiates an ``HttpResponse`` object with the given page content and
634 640
     content type.
@@ -646,8 +652,12 @@ Methods
646 652
 
647 653
     Historically, this parameter was called ``mimetype`` (now deprecated).
648 654
 
649  
-    ``status`` is the `HTTP Status code`_ for the response.
  655
+    ``status`` is the `HTTP status code`_ for the response.
  656
+
  657
+    .. versionadded:: 1.6
650 658
 
  659
+    ``reason`` is the HTTP response phrase. If not provided, a default phrase
  660
+    will be used.
651 661
 
652 662
 .. method:: HttpResponse.__setitem__(header, value)
653 663
 
@@ -727,8 +737,7 @@ Methods
727 737
 
728 738
     This method makes an :class:`HttpResponse` instance a file-like object.
729 739
 
730  
-.. _HTTP Status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10
731  
-
  740
+.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10
732 741
 
733 742
 .. _ref-httpresponse-subclasses:
734 743
 
@@ -851,7 +860,13 @@ Attributes
851 860
 
852 861
 .. attribute:: HttpResponse.status_code
853 862
 
854  
-    The `HTTP Status code`_ for the response.
  863
+    The `HTTP status code`_ for the response.
  864
+
  865
+.. attribute:: HttpResponse.reason_phrase
  866
+
  867
+    .. versionadded:: 1.6
  868
+
  869
+    The HTTP reason phrase for the response.
855 870
 
856 871
 .. attribute:: HttpResponse.streaming
857 872
 
2  docs/releases/1.6.txt
@@ -241,6 +241,8 @@ Minor features
241 241
 * The ``choices`` argument to model fields now accepts an iterable of iterables
242 242
   instead of requiring an iterable of lists or tuples.
243 243
 
  244
+* The reason phrase can be customized in HTTP responses.
  245
+
244 246
 Backwards incompatible changes in 1.6
245 247
 =====================================
246 248
 
0  tests/responses/__init__.py
No changes.
0  tests/responses/models.py
No changes.
15  tests/responses/tests.py
... ...
@@ -0,0 +1,15 @@
  1
+from django.http import HttpResponse
  2
+import unittest
  3
+
  4
+class HttpResponseTests(unittest.TestCase):
  5
+
  6
+    def test_status_code(self):
  7
+        resp = HttpResponse(status=418)
  8
+        self.assertEqual(resp.status_code, 418)
  9
+        self.assertEqual(resp.reason_phrase, "I'M A TEAPOT")
  10
+
  11
+    def test_reason_phrase(self):
  12
+        reason = "I'm an anarchist coffee pot on crack."
  13
+        resp = HttpResponse(status=814, reason=reason)
  14
+        self.assertEqual(resp.status_code, 814)
  15
+        self.assertEqual(resp.reason_phrase, reason)

0 notes on commit cb86f70

Please sign in to comment.
Something went wrong with that request. Please try again.