/circuits/web/errors.py

https://bitbucket.org/prologic/circuits/ · Python · 208 lines · 163 code · 11 blank · 34 comment · 18 complexity · 584bbcf4e6b94d7ca57264e1ba17780d MD5 · raw file

  1. # Module: errors
  2. # Date: 11th February 2009
  3. # Author: James Mills, prologic at shortcircuit dot net dot au
  4. """Errors
  5. This module implements a set of standard HTTP Errors.
  6. """
  7. from cgi import escape
  8. try:
  9. from urllib.parse import urljoin as _urljoin
  10. except ImportError:
  11. from urlparse import urljoin as _urljoin # NOQA
  12. from circuits import Event
  13. from ..six import string_types
  14. from .constants import SERVER_URL, SERVER_VERSION
  15. from .constants import DEFAULT_ERROR_MESSAGE, HTTP_STATUS_CODES
  16. class httperror(Event):
  17. """An event for signaling an HTTP error"""
  18. code = 500
  19. description = ""
  20. contenttype = "text/html"
  21. def __init__(self, request, response, code=None, **kwargs):
  22. """
  23. The constructor creates a new instance and modifies the *response*
  24. argument to reflect the error.
  25. """
  26. super(httperror, self).__init__(request, response, code, **kwargs)
  27. # Override HTTPError subclasses
  28. self.name = "httperror"
  29. self.request = request
  30. self.response = response
  31. if code is not None:
  32. self.code = code
  33. self.error = kwargs.get("error", None)
  34. self.description = kwargs.get(
  35. "description", getattr(self.__class__, "description", "")
  36. )
  37. if self.error is not None:
  38. self.traceback = "ERROR: (%s) %s\n%s" % (
  39. self.error[0], self.error[1], "".join(self.error[2])
  40. )
  41. else:
  42. self.traceback = ""
  43. self.response.close = True
  44. self.response.status = self.code
  45. self.response.headers["Content-Type"] = self.contenttype
  46. self.data = {
  47. "code": self.code,
  48. "name": HTTP_STATUS_CODES.get(self.code, "???"),
  49. "description": self.description,
  50. "traceback": escape(self.traceback),
  51. "url": SERVER_URL,
  52. "version": SERVER_VERSION
  53. }
  54. def sanitize(self):
  55. if self.code != 201 and not (299 < self.code < 400):
  56. if "Location" in self.response.headers:
  57. del self.response.headers["Location"]
  58. def __str__(self):
  59. self.sanitize()
  60. return DEFAULT_ERROR_MESSAGE % self.data
  61. def __repr__(self):
  62. return "<%s %d %s>" % (
  63. self.__class__.__name__, self.code, HTTP_STATUS_CODES.get(
  64. self.code, "???"
  65. )
  66. )
  67. class forbidden(httperror):
  68. """An event for signaling the HTTP Forbidden error"""
  69. code = 403
  70. class unauthorized(httperror):
  71. """An event for signaling the HTTP Unauthorized error"""
  72. code = 401
  73. class notfound(httperror):
  74. """An event for signaling the HTTP Not Fouond error"""
  75. code = 404
  76. class redirect(httperror):
  77. """An event for signaling the HTTP Redirect response"""
  78. def __init__(self, request, response, urls, code=None):
  79. """
  80. The constructor creates a new instance and modifies the
  81. *response* argument to reflect a redirect response to the
  82. given *url*.
  83. """
  84. if isinstance(urls, string_types):
  85. urls = [urls]
  86. abs_urls = []
  87. for url in urls:
  88. # Note that urljoin will "do the right thing" whether url is:
  89. # 1. a complete URL with host (e.g. "http://www.example.com/test")
  90. # 2. a URL relative to root (e.g. "/dummy")
  91. # 3. a URL relative to the current path
  92. # Note that any query string in request is discarded.
  93. url = request.uri.relative(url).unicode()
  94. abs_urls.append(url)
  95. self.urls = urls = abs_urls
  96. # RFC 2616 indicates a 301 response code fits our goal; however,
  97. # browser support for 301 is quite messy. Do 302/303 instead. See
  98. # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
  99. if code is None:
  100. if request.protocol >= (1, 1):
  101. code = 303
  102. else:
  103. code = 302
  104. else:
  105. if code < 300 or code > 399:
  106. raise ValueError("status code must be between 300 and 399.")
  107. super(redirect, self).__init__(request, response, code)
  108. if code in (300, 301, 302, 303, 307):
  109. response.headers["Content-Type"] = "text/html"
  110. # "The ... URI SHOULD be given by the Location field
  111. # in the response."
  112. response.headers["Location"] = urls[0]
  113. # "Unless the request method was HEAD, the entity of the response
  114. # SHOULD contain a short hypertext note with a hyperlink to the
  115. # new URI(s)."
  116. msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
  117. 301: ("This resource has permanently moved to "
  118. "<a href='%s'>%s</a>."),
  119. 302: ("This resource resides temporarily at "
  120. "<a href='%s'>%s</a>."),
  121. 303: ("This resource can be found at "
  122. "<a href='%s'>%s</a>."),
  123. 307: ("This resource has moved temporarily to "
  124. "<a href='%s'>%s</a>."),
  125. }[code]
  126. response.body = "<br />\n".join([msg % (u, u) for u in urls])
  127. # Previous code may have set C-L, so we have to reset it
  128. # (allow finalize to set it).
  129. response.headers.pop("Content-Length", None)
  130. elif code == 304:
  131. # Not Modified.
  132. # "The response MUST include the following header fields:
  133. # Date, unless its omission is required by section 14.18.1"
  134. # The "Date" header should have been set in Response.__init__
  135. # "...the response SHOULD NOT include other entity-headers."
  136. for key in ("Allow", "Content-Encoding", "Content-Language",
  137. "Content-Length", "Content-Location", "Content-MD5",
  138. "Content-Range", "Content-Type", "Expires",
  139. "Last-Modified"):
  140. if key in response.headers:
  141. del response.headers[key]
  142. # "The 304 response MUST NOT contain a message-body."
  143. response.body = None
  144. # Previous code may have set C-L, so we have to reset it.
  145. response.headers.pop("Content-Length", None)
  146. elif code == 305:
  147. # Use Proxy.
  148. # urls[0] should be the URI of the proxy.
  149. response.headers["Location"] = urls[0]
  150. response.body = None
  151. # Previous code may have set C-L, so we have to reset it.
  152. response.headers.pop("Content-Length", None)
  153. else:
  154. raise ValueError("The %s status code is unknown." % code)
  155. def __repr__(self):
  156. if len(self.channels) > 1:
  157. channels = repr(self.channels)
  158. elif len(self.channels) == 1:
  159. channels = str(self.channels[0])
  160. else:
  161. channels = ""
  162. return "<%s %d[%s.%s] %s>" % (
  163. self.__class__.__name__, self.code, channels, self.name,
  164. " ".join(self.urls)
  165. )